/yuppy

Python Programming for the Privileged Class

Primary LanguagePythonMIT LicenseMIT

Yuppy

Python Programming for the Privileged Class


Yuppy is released under the MIT License.

Yuppy is a small Python library that integrates seamlessly with your application to promote data integrity by supporting common object-oriented language features. It intends to provide fully integrated support for interfaces, abstract classes and methods, final classes and methods, and type hinting in a manner that preserves much of the dynamic nature of Python. Yuppy can improve the integrity of your data and the stability of your code without comprimising usability. It is easy to use and is intentionally designed to fit with the Python development culture, not circumvent it.

Table of contents


  1. Introduction
  2. A Complete Example
  3. Class Decorators
  4. Member Decorators
  5. Interfaces
  6. Type Hinting
"But type checking is bad!"

Yuppy does type checking in a manner that is in keeping with the dynamic nature of Python. Yuppy interface checks can be based on duck-typing, so any class can serve as a Yuppy interface. This feature simply serves as a more efficient way to determine whether any given object walks and talks like a duck.

A Complete Example

from yuppy import *

# Yuppy classes must either use the base yuppy.ClassType metaclass or use
# the @yuppy.yuppy decorator.
# In this case, we're creating an abstract base class "Apple" with a
# couple of abstract methods for getting attributes.
@abstract
class Apple(object):
  """An abstract apple."""
  @abstract
  def get_color(self):
    """Gets the apple color."""

  @abstract
  def set_color(self):
    """Sets the apple color."""

# Create an interface for objects that can be eaten.
@interface
class IEatable(object):
  """An interface that supports eating."""
  def eat(self):
    """Eats an apple."""

# Now, we can implement a concret "GreenApple" class. Note that we must
# implement the abstract get_color() and set_color() methods or else we
# have to explicitly declare the class once again @abstract. Similarly,
# we are implementing the IEatable interface, so we must implement all
# the IEatable methods or else declare the class abstract.
@implements(IEatable)
class GreenApple(Apple):
  """A concrete green apple."""
  # Don't allow the green apple color to be changed.
  color = const('green')

  # Create a float weight.
  weight = var(float)

  # Override the get_color() abstract method.
  def get_color(self):
    return self.color

  # Override the set_color() abstract method. We'll just raise an error.
  def set_color(self, value):
    raise AttributeError("Cannot set green apple color.")

  # Implement a method to set the green apple weight. Here we use type
  # hinting to ensure that the weight argument is an integer or float.
  # Note that we can also use interfaces for type hinting, including any
  # class that does not extend the yuppy.Interface class (which results
  # in an attribute-based comparison, e.g. duck typing).
  @params(weight=(int, float))
  def set_weight(self, weight):
    self.weight = weight

# Implement an implicit interface.
class ITree(object):
  def add_apple(self, apple):
    """Adds an apple to the tree."""
  def remove_apple(self, apple):
    """Removes an apple from the tree."""

# Even without extending the Interface class, we can use ITree as an interface.
@implements(ITree)
class Tree(object):
  apples = var(set)

  def __init__(self):
    self.apples = set()

  # We can use any class or interface for type hinting. If an Apple instance
  # is not passed as the 'apple' argument, the argument will be compared to
  # the Apple class to determine whether it is equivalent by attributes.
  @params(apple=Apple)
  def add_apple(self, apple):
    self.apples.add(apple)

  @params(apple=Apple)
  def remove_apple(self, apple):
    self.apples.remove(apple)

Of course, in the real world this would be an unrealistic example. Python's flexibility and features like data descriptors remove the need for getters and setters that are necessary in other languages (this is one reason that Yuppy does not attempt encapsulation). But, indeed, these features can be very useful in ultimately reducing the code required for error handling by helping ensure the integrity of data from the time it is set on an object or passed to an instance method.

Class Decorators

yuppy

Declares a Yuppy class definition.

yuppy(cls)

This decorator is not required to implement a Yuppy class. The recommended alternative to using the yuppy decorator is to use the yuppy.ClassType metaclass in your class definition. The decorator simply dynamically extends any class to use the yuppy.ClassType metaclass in its definition.

from yuppy import ClassType, yuppy

@yuppy
class Apple(object):
  """This is a Yuppy class."""

class Apple(object):
  """This is also a Yuppy class."""
  __metaclass__ = ClassType

abstract

Creates an abstract class.

abstract(cls)

Abstract classes are classes that cannot themselves be instantiated, but can be extended and instantiated. An abstract class can contain any number of abstract methods. When an abstract class is extended, the extending class must override all the abstract methods or else declare itself abstract.

Example
from yuppy import abstract

@abstract
class Apple(object):
  """An abstract apple."""
  weight = var(float)

  def get_weight(self):
    return self.weight

  def set_weight(self, weight):
    self.weight = weight

class GreenApple(Apple):
  """A concrete green apple."""

We will be able to create instances of GreenApple, which inherits from Apple, but any attempts to instantiate an Apple will result in a TypeError.

>>> apple = GreenApple()
>>> apple.set_weight(1.0)
>>> apple.get_weight()
1.0
>>> apple = Apple()
TypeError: Cannot instantiate abstract class 'Apple'.

final

Declares a class definition to be final.

The final Yuppy decorator is, well, final, which allows users to define classes that cannot be extended. This is a common feature in several other object-oriented languages.

final(cls)
Example
from yuppy import final

@final
class Apple(object):
  weight = var(float, default=None)
>>> apple = Apple()
>>> class GreenApple(Apple):
...   pass
...
TypeError: ...

Member Decorators

variable

Creates a variable attribute.

variable([default=None[, validate=None[, *types]]])
var([default=None[, validate=None[, *types]]])
Example
from yuppy import yuppy, var

@yuppy
class Apple(object):
  foo = var(int, default=None, validate=lambda x: x == 1)
>>> apple = Apple()

static

Creates a static attribute.

Static Yuppy members are equivalent to standard Python class members. This is essentially the same parallel that exists between Python's class members and static variables in many other object-oriented languages. With Yuppy we can use the static decorator to create static methods or properties.

static([default=None[, validate=None[, *types]]])
Example
from yuppy import yuppy, static

@yuppy
class Apple(object):
  """An abstract apple."""
  weight = static(float, default=None)

With static members, changes to a member variable will be applied to all instances of the class. So, even after instantiating a new instance of the class, the weight attribute value will remain the same.

>>> apple1 = Apple()
>>> apple1.weight
None
>>> apple1.weight = 2.0
>>> apple1.weight
2.0
>>> apple2 = Apple()
>>> apple2.weight
2.0

constant

Creates a constant attribute.

Constants are attributes which have a permanent value. They can be used for any value which should never change within the application, such as an application port number, for instance. With Yuppy we can use the const decorator to create a constant, passing a single permanent value to the constructor.

constant(value)
const(value)
Example
from yuppy import yuppy, const

@yuppy
class RedApple(object):
  color = const('red')
>>> RedApple.color
'red'
>>> apple = RedApple()
>>> apple.color
'red'
>>> RedApple.color = 'blue'
AttributeError: Cannot override 'RedApple' attribute 'color' by assignment.
>>> RedApple.color
'red'
>>> apple.color
'red'
>>> apple = RedApple()
>>> apple.color
'red'
>>> apple.color = 'blue'
AttributeError: Cannot override 'Apple' attribute 'color' by assignment.

method

Creates a method attribute.

method(callback)
Example
from yuppy import yuppy, var, method

@yuppy
class Apple(object):
  color = var(default='red')

  @method
  def getcolor(self):
    return self.color
>>> apple = Apple()
>>> apple.getcolor()
'red'

abstract

Creates an abstract method.

abstract(method)

Abstract methods can be applied to any python class, even without declaring the class to be abstract. This means that if the method is not re-defined in a child class, an AttributeError will be raised if the abstract method is accessed. Therefore, it is strongly recommended that any class that contains abstract methods be declared abstract.

Example
from yuppy import abstract

@abstract
class Apple(object):
  """An abstract apple."""
  @abstract
  def get_color(self):
    """Gets the apple color."""

Once we've defined an abstract class, we can extend it and override the abstract methods.

>>> class GreenApple(object):
...   def get_color(self):
...     return 'green'
...
>>> apple = GreenApple()
>>> apple.get_color()
'green'

Note what happens if we try to use abstract methods or fail to override them.

>>> class GreenApple(object):
...   pass
...
>>> # We can still instantiate green apples since the class isn't declared abstract.
>>> apple = GreenApple()
TypeError: Cannot instantiate abstract class 'GreenApple'.

final

Creates a final method.

final(method)

Similar to final classes, final methods cannot be overridden. When a class attempts to override a final method, a TypeError will be raised.

Example
from yuppy import yuppy, final

@yuppy
class RedApple(object):
  @final
  def getcolor(self):
    return 'red'
>>> class BlueApple(RedApple):
...   def getcolor(self):
...     return 'blue'
...
TypeError: Cannot override final 'RedApple' method 'getcolor'.

Type Validation

Yuppy can perform direct type checking and arbitrary execution of validation callbacks. When a mutable Yuppy attribute is set, validators will automatically be executed. This ensures that values are validated at the time they're set rather than when they're accessed.

Any var type can perform data validation. When creating a new var, we can pass either <type> or validate=<func> to the object constructor.

Example
from yuppy import yuppy, var

@yuppy
class Apple(object):
  """An abstract apple."""
  weight = var(float)

  def __init__(self, weight):
    self.weight = weight

Now, if we create an Apple instance we can see how the validation works. Note that if an improper value is passed to the constructor, the validator will automatically try to cast it to the correct type if only one type is provided.

>>> apple = Apple(1)
>>> apple = Apple('one')
AttributeError: Invalid attribute value for 'weight'.

Note also that instance variable type checking is integrated with the Yuppy interface system. This means that an interface can be passed to any variable definition as the type argument, and Yuppy will validate variable values based on duck typing. This can be very useful within the context of the Python programming language.

Interfaces

Interfaces are a partcilarly useful feature with Python. Since Python promotes duck typing, Yuppy interfaces can be used to ensure that any object walks and talks like a duck. For this reason, Yuppy interface evaluation supports both explicit interface implementation checks and implicit interface implementation checks, or duck typing.

interface

Creates a Yuppy interface.

The yuppy.interface decorator is the equivalent of yuppy.yuppy for interfaces. The decorator simply wraps the given class and declares the yuppy.InterfaceType metaclass. Abstract interface attributes are declared by simply creating them. Yuppy will evaluate the interface for any public attributes and consider those to be required of any implementing classes.

Example
from yuppy import interface

@interface
class AppleInterface(object):
  """An apple interface."""
  def get_color(self):
    """Returns the apple color."""

  def get_weight(self):
    """Returns the apple weight."""

Note that the yuppy.Interface class is not required to create an interface. Yuppy can do interface checking based on duck-typing. Simply defining any class and passing it to the yuppy.instanceof method will results in a simple comparison of each object's attributes.

For example:

class IFoo(object):
  def foo(self):
    pass
  def bar(self):
    pass

An instance of any class that contains the methods foo and bar will be considered an instance of the IFoo interface.

>>> from yuppy import instanceof
>>> class Foo(object):
...   def foo(self):
...     pass
...   def bar(self):
...     pass
...
>>> foo = Foo()
>>> instanceof(foo, IFoo)
True

implements

Declares a class definition to implement an interface.

When a class implements an interface, it must define all abstract attributes of that interface. Yuppy will automatically evaluate the class definition to ensure it conforms to the indicated interface.

Example

Continuing with the previous example, we can implement the AppleInterface interface.

>>> from yuppy import implements
>>> @implements(AppleInterface)
... class Apple(object):
...   """An apple."""
...   __metaclass__ = ClassType
...
TypeError: 'Apple' contains an abstract method 'get_color' and must be declared abstract.

Note that if we don't implement the AppleInterface attributes a TypeError will be raised. Let's try that again.

>>> from yuppy import implements, const
>>> @implements(AppleInterface)
... class Apple(object):
...   """An apple."""
...   color = const('red')
...   weight = const(2.0)
...   def get_color(self):
...     """Returns the apple color."""
...     return self.color
...   def get_weight(self):
...     """Returns the apple weight."""
...     return self.weight
...
>>> apple = Apple()
>>> apple.get_color()
'red'

instanceof

Determines whether an instance's class implements an interface.

implements(instance, interface[, ducktype=True])

Finally, it's important that we be able to evaluate objects for adherence to any interface requirements. The instanceof function behaves similarly to Python's built-in isinstance function, but for Yuppy interfaces. However, Yuppy's implementation can also evaluate interface implementation based on duck typing. This means that object classes do not necessarily have to implement a specific interface, they simply need to behave in the manner that the interface requires.

>>> from yuppy import instanceof
>>> apple = Apple()
>>> instanceof(apple, AppleInterface)
True
>>> instanceof(apple, AppleInterface, False)
True
>>> instanceof(apple, Apple)
True

Type Hinting

With Yuppy providing all these type checking features, that would normally mean a lot more calls to the Yuppy API to validate data. But luckily Yuppy provides an API for that, as well. Yuppy uses decorators to perform type hinting for method parameters.

params

Defines method parameter types.

The params decorator can set required parameter types using any python class or Yuppy interface. This allows for flexible type checking based on either isinstance or straight duck-typing.

from yuppy import yuppy, params

@yuppy
class Apple(object):
  """A base apple."""
  color = var(basestring, default='red')

  @params(color=basestring)
  def set_color(self, color='red'):
    self.color = color

  @params(weight=(float, int))
  def set_weight(self, weight):
    self.weight = weight

If we pass invalid values to type hinted methods, a TypeError will be raised.

>>> apple = Apple()
>>> apple.set_color('blue')
>>> apple.set_color(1)
TypeError: Method argument 'color' must be an instance of '<type 'basestring'>'.
>>> # Note that it still handles default arguments, as well.
>>> apple.set_color()
>>> apple.set_weight('two')
TypeError: Method argument 'weight' must be an instance of '(<type 'float'>, <type 'int'>)'.

Also, remember that Yuppy type checking can be interface-based.

from yuppy import yuppy, params

class IApple(object):
  def get_color(self):
    pass
  def get_weight(self):
    pass

@yuppy
class AppleTree(object):
  def __init__(self):
    self.apples = []

  @params(apple=IApple)
  def add_apple(self, apple):
    self.apples.append(apple)
>>> tree = AppleTree()
>>> tree.add_apple('foo')
TypeError: Method argument 'apple' must be an instance of 'IApple'.
>>> from yuppy import ClassType
>>> class Apple(Object):
...   __metaclass__ = ClassType
...   def get_color(self):
...     return 'red'
...   def get_weight(self):
...     return 1.0
...
>>> apple = Apple()
>>> tree.add_apple(apple)
>>> # success!

Pull requests welcome!

Copyright (c) 2013 Jordan Halterman