Copying methods in Python

17 02 2007

Doctests should be self-explanatory:

class Copycat(object):
    def extract(self, klass, method):
        """Extract a `method` from given `klass` so it can be used on this object.

        >>> class A(object):
        ...    def inc(self, n):
        ...        return n + 1
        >>> p = Copycat()
        >>> p.extract(A, 'inc')(5)
        6
        """
        return lambda *args, **kwds: klass.__dict__[method](self, *args, **kwds)

    def copy_method(self, klass, method):
        """Copy a method from `klass` into this object.

        >>> class A(object):
        ...     def inc(self, n):
        ...         return n + 1
        >>> p = Copycat()
        >>> p.inc(11)
        Traceback (most recent call last):
          ...
        AttributeError: 'Copycat' object has no attribute 'inc'
        >>> p.copy_method(A, 'inc')
        >>> p.inc(11)
        12
        """
        self.__dict__[method] = self.extract(klass, method)

    def copy_methods(self, klass, *methods):
        """Copy methods from `klass` into this object.
        """
        for method in methods:
            self.copy_method(klass, method)

Normally you can’t call foreign method on a given object, so this won’t work:

>>> class A(object):
...     def inc(self, n):
...             return n + 1
>>> class B(object): pass
>>> b = B()
>>> A.inc(b, 7)
Traceback (most recent call last):
  File "", line 1, in ?
TypeError: unbound method inc() must be called with A instance as first argument (got B instance instead)

Using the trick with __dict__ you can overcome the type check and use the power of duck typing:

>>> A.__dict__['inc'](b, 7)
8

I used this technique with Mock class (from python-mock module) to easily test particular methods of a class, while mocking others. Let me give an example.

Imagine you’re testing a class which has few methods that touch the filesystem/database (so they are slow and rely on external unpredictable state) and few that are pure and do some logic, while delegating all the dirty work to the first group of methods. For example:

class ClassWeWantToTest(object):
    def __init__(self):
        self.that = self._init_that()

    def _init_that(self):
        # Touching the filesystem/database/...
        pass

    def dirty_work(self, argument):
        # Touching the filesystem/database/...
        pass

    def referentially_transparent(self, argument):
        # Working on argument and calling self.dirty_work()
        #  from time to time.
        pass

_init_that and dirty_work are “dirty”, while referentially_transparent is “pure”. Now, we want to test the referentially_transparent. We can’t make a mock and call referentially_transparent on it:

def test_it():
    m = Mock()
    m.that = [1,3,5,42]

    ClassWeWantToTest.referentially_transparent(m, 13)

This won’t work for the same reason the example with A and B classes didn’t work:

Traceback (most recent call last):
  File "", line 7, in ?
    test_it()
  File "", line 5, in test_it
    ClassWeWantToTest.referentially_transparent(m, 13)
TypeError: unbound method referentially_transparent() must be called with ClassWeWantToTest instance as first argument (got Mock instance instead)

This is where Copycat class comes in. Using the class we can rewrite test code into this:

class CopycatMock(Mock, Copycat): pass

def test_it():
    m = CopycatMock()
    m.that = [1,3,5,42]
    m.copy_method(ClassWeWantToTest, 'referentially_transparent')

    m.referentially_transparent(13)

Now everything seems to work. We can mock return values and set expectations as usual. Happy mocking!

Note: To make it work with current (0.1.0) version of python-mock, use this small patch.

Advertisements

Actions

Information

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s




%d bloggers like this: