Fork me on GitHub
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.

Features

Installing pyduck

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.


Interfaces

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:

Validating arguments (@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.

Function overloading

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.

Validating return values (@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 .