sihrc sihrc - 4 months ago 10
Python Question

Inheriting a patched class

I have a base class extending unittest.TestCase, and I want to patch that base class, such that classes extending this base class will have the patches applied as well.

Code Example:

@patch("some.core.function", mocked_method)
class BaseTest(unittest.TestCase):
#methods
pass

class TestFunctions(BaseTest):
#methods
pass


Patching the
TestFunctions
class directly works, but patching the BaseTest class does not change the functionality of
some.core.function
in
TestFunctions
.

Answer

You probably want a metaclass here: a metaclass simply defines how a class is created. By default, all classes are created using Python's built-in class type:

>>> class Foo:
...     pass
...
>>> type(Foo)
<class 'type'>
>>> isinstance(Foo, type)
True

So classes are actually instances of type. Now, we can subclass type to create a custom metaclass (a class that creates classes):

class PatchMeta(type):
    """A metaclass to patch all inherited classes."""

We need to control the creation of our classes, so we wanna override the type.__new__ here, and use the patch decorator on all new instances:

class PatchMeta(type):
    """A metaclass to patch all inherited classes."""

    def __new__(meta, name, bases, attrs):
        cls = type.__new__(meta, name, bases, attrs)
        cls = patch("some.core.function", mocked_method)(cls)
        return cls

And now you simply set the metaclass using __metaclass__ = PatchMeta:

class BaseTest(unittest.TestCase):
    __metaclass__ = PatchMeta
    # methods

The issue is this line:

cls = patch("some.core.function", mocked_method)(cls)

So currently we always decorate with arguments "some.core.function" and mocked_method. Instead you could make it so that it uses the class's attributes, like so:

cls = patch(*cls.patch_args)(cls)

And then add patch_args to your classes:

class BaseTest(unittest.TestCase):
    __metaclass__ = PatchMeta
    patch_args = ("some.core.function", mocked_method)

Edit: As @mgilson mentioned in the comments, patch() modifies the class's methods in place, instead of returning a new class. Because of this, we can replace the __new__ with this __init__:

class PatchMeta(type):
    """A metaclass to patch all inherited classes."""

    def __init__(cls, *args, **kwargs):
        super(PatchMeta, self).__init__(*args, **kwargs)
        patch(*cls.patch_args)(cls)

Which is quite unarguably cleaner.

Comments