pyduck
pyduck is an utility framework for easier, more robust and effective usage of one of Python's defining language traits: duck typing. It enhances the Python language with several useful features.
Source code and issue tracking for pyduck can be found on GitHub.
@expects)@returns)Install using either pip or easy_install:
pip install pyduck
easy_install pyduck
or - if you want the bleeding edge - download the latest version from GitHub:
git clone git://github.com/Xion/pyduck.git
cd pyduck
python setup.py develop
This way you can git pull changes without having to run setup.py again.
pyduck allows to define interfaces which specify a desired set of methods
that we require for objects to expose. For example, we can introduce a Serializer
interface, containing methods for loading and dumping objects from/to some external data format
(such as JSON or YAML):
from pyduck import Interface
class Serializer(Interface):
&tab;def load(fp): pass
&tab;def loads(s): pass
&tab;def dump(obj, fp): pass
&tab;def dumps(obj): pass
Once there, we can use it in code that is supposed to operate on such serializers:
from pyduck import implements
class Data(object):
&tab;def __init__(self, serializer):
&tab;&tab;if not implements(serializer, Serializer):
&tab;&tab;&tab;raise TypeError, "Invalid serializer"
&tab;&tab;self._serializer = serializer
&tab;# ...
The trick is that object passed to Data.__init__ does not need to know anything
about the Serializer interface. The implements test will check
for actual methods (load, loads, dump
and dumps) rather than any explicit declarations - unlike, for example,
abstract base classes.
To be more specific: when it comes to deciding whether object implements an interface, pyduck will check:
*args) or keyword (**kwargs))@expects)In the example above, an if is used to check whether argument given
to Data.__init__ implements desired interface. Such constructs generally look
like boilerplate, and pyduck helps to eliminate them by offering
the @expects decorator.
from pyduck import expects
class Data(object):
&tab;@expects(Serializer)
&tab;def __init__(self, serializer):
&tab;&tab;self._serializer = serializer
&tab;# ...
@expects allows to specify interfaces or types which function arguments are required
to "fit" into. This translates into implements or isinstance checks when
function is called. Should any of those checks fail, pyduck.ArgumentError
(derived from TypeError) is raised.
In Python, it's quite common pattern to provide slightly different logic depending on arguments the function was called with:
def as_json(value):
&tab;if isinstance(value, basestring):
&tab;&tab;return '"%s"' % str(value)
&tab;if isinstance(value, list):
&tab;&tab;json_list = map(as_json, value)
&tab;&tab;return "[%s]" % str.join(",", json_list)
&tab;if isinstance(value, dict):
&tab;&tab;json_dict = dict( ((as_json(str(k)), as_json(v))
&tab;&tab;&tab;&tab;&tab;&tab;&tab;&tab;for k, v in value.iteritems()) )
&tab;&tab;json_dict_string = str.join(",",
&tab;&tab;&tab;&tab;&tab;&tab;&tab;&tab;("%s: %s" for k, v in json_dict.iteritems()))
&tab;&tab;return "{%s}" % json_dict_string
&tab;# etc.
This is another case of boilerplate ifs cluttering the function's code.
To eliminate them, pyduck allows to split the function into distinct
routines - each having appropriate @expects declaration - and then combine them
into single "overloaded" function:
@expects(basestring)
def __string_as_json(value):
&tab;return '"%s"' % str(value)
@expects(list)
def __list_as_json(value):
&tab;json_list = map(as_json, value)
&tab;return "[%s]" % str.join(",", json_list)
@expects(dict)
def __dict_as_json(value)
&tab;json_dict = dict( ((as_json(str(k)), as_json(v))
&tab;&tab;&tab;&tab;&tab;&tab;&tab;for k, v in value.iteritems()) )
&tab;json_dict_string = str.join(",",
&tab;&tab;&tab;&tab;&tab;&tab;&tab;("%s: %s" for k, v in json_dict.iteritems()))
&tab;return "{%s}" % json_dict_string
as_json = overload(__string_as_json, __list_as_json, __dict_as_json)
When such function is called, pyduck will check how well the @expects
declarations are matched against actual arguments. The call is then dispatched to version
that "wins" the overload resolution.
@returns)To complement the @expects decorator, pyduck offers the
@returns one. It can automatically verify whether function has returned an object
of correct interface or type:
from pyduck import returns
@returns(int)
def get_random_number():
&tab;return 4 # chosen by fair dice roll, guaranteed to be random
While the verification is slightly less useful than the one performed by @expects,
@returns serves well as a visual aid: it concisely informs the callers
what they can expect for function to return. And in case changes in function code cause
the @returns contract to break, pyduck.ReturnValueError
(derived from TypeError) will be raised when returning from function call .