Thursday, November 8, 2012

Duck Typing Assert in Python

People who come from strongly typed languages that offer interfaces often are confused by lack of one in Python. Python, being dynamic typing programming language, follows duck typing principal. Here we will see how programmer can assert duck typing between two Python classes. Setup environment before proceed:
$ virtualenv env
$ env/bin/pip install wheezy.core
Let play a bit with duck test `looks like`. Place the following code snippet into file `test.py`:
from wheezy.core.introspection import looks

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

class Foo(object):
    def bar(self):
        pass

assert looks(Foo).like(IFoo)
and run it:
test.py:11: UserWarning: 'foo': is missing.
  assert looks(Foo).like(IFoo)
Traceback (most recent call last):
  File "test.py", line 11, in 
    assert looks(Foo).like(IFoo)
AssertionError
The error says the function `foo` is missing in `Foo` implementation. Let fix that and look at arguments checks.
from wheezy.core.introspection import looks

class IFoo(object):
    def foo(self, a, b=None):
        pass

class Foo(object):
    def foo(self):
        pass

assert looks(Foo).like(IFoo)
Here is output from run:
test.py:11: UserWarning: 'foo': argument names or defaults have no match.
  assert looks(Foo).like(IFoo)
Traceback (most recent call last):
  File "test.py", line 11, in 
    assert looks(Foo).like(IFoo)
AssertionError
So far so good. Let fix it and take a look at properties:
from wheezy.core.introspection import looks

class IFoo(object):

    def foo(self, a, b=None):
        pass
        
    @property
    def bar(self):
        pass


class Foo(object):

    def foo(self, a, b=None):
        pass

    def bar(self):
        pass

assert looks(Foo).like(IFoo)
Here is output:
test.py:21: UserWarning: 'bar': is not property.
  assert looks(Foo).like(IFoo)
Traceback (most recent call last):
  File "test.py", line 21, in 
    assert looks(Foo).like(IFoo)
AssertionError
Look at other examples available here. A simple `duck test` using `looks like` approach asserts conformance between two classes in Python.

10 comments:

  1. People coming from other language might also find interesting zope.interface and abc in Python 3.

    ReplyDelete
  2. Nice!

    This begs a question - why do we have isinstance but not this kind of support in the language?

    ReplyDelete
  3. Related discussion on comp.lang.python - https://groups.google.com/group/comp.lang.python/browse_thread/thread/6cfbf2b69f4a441f?hl=en#

    ReplyDelete
  4. Replies
    1. This is not the same thing. It doesn't require me to subclass anything, thus object is light. I do no need run-time checks only a single time once the module is imported. It works with any version of python.

      Delete
  5. Wait, so this is supposed to fail?! (From your tests)

    class IFoo(object):
    ..def __len__(self):
    ....pass

    class Foo(IFoo):
    ..pass

    assert not looks(Foo).like(IFoo, notice=['__len__'])
    self.assert_warning("'__len__': is missing.")

    Is Foo not supposed to be a subclass of IFoo? Because as your tests stand now, len(Foo) would work (or, rather, would throw the same TypeError that len(IFoo) does).

    ReplyDelete
    Replies
    1. `assert not` will not fail, however you will see warning message, as expected.

      Delete
  6. After looking at the code, I see that attributes of parent classes aren't examined, which makes this effort incomplete. An empty subclass with will be reported as failing to matching its parent's interface, which is obviously not the case.

    ReplyDelete
    Replies
    1. The looks like checks are explicit. If you need to look at whole hierarchy tree just list them all in separate asserts. This adds readability to your code.

      Delete