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.
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.
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.
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
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.
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'.
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)
from yuppy import final
@final
class Apple(object):
weight = var(float, default=None)
>>> apple = Apple()
>>> class GreenApple(Apple):
... pass
...
TypeError: ...
Creates a variable attribute.
variable([default=None[, validate=None[, *types]]])
var([default=None[, validate=None[, *types]]])
from yuppy import yuppy, var
@yuppy
class Apple(object):
foo = var(int, default=None, validate=lambda x: x == 1)
>>> apple = Apple()
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]]])
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
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)
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.
Creates a method attribute.
method(callback)
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'
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.
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'.
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.
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'.
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.
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 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.
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.
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
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.
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'
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
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.
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