Fork me on GitHub

Project Notes

#040 Decorating Class Methods

Decorating class methods in Python is on the one hand no different than decorating a function. But due to the minimalistic way that classes are implemented, performing class-specific operations within a decorator requires a bit of work.

Handling self and Arbitrary Parameters

Essentially, all that is required to decorate classes is to explicitly expect self as the first argument to the function.

I you don’t really care about the self reference inside the decorator, then it is sufficient to define the decorator function to expect arbitrary arguments:

def decorator(func):
    def wrapper(*args):
        return func(*args)
    return wrapper

In the example, I’m explicitly expecting self so I can easily do things with it in the decorator:

def method_wrapper(func):
    def wrapper(self, *args, **kwargs):
        return func(self, *args, **kwargs)
    return wrapper

Allowing Introspection of Decorated Functions

Decorating a function has an unfortunate side-effect that details of the original function are lost for introspection. This includes the fact that __doc__ and __name__ now return values for the decorator function, not the original method.

Sometimes this may not matter, but sometimes it really does. For example, if you are using django-rest-swagger, then decorators can break it’s ability to gereate swagger documantation from your API methods.

Fortunately, functools.wraps provides a simple fix. It restores the __doc__ and __name__ methods to return details form the original function.

It just requires a decorator for the decorator(!)..

from functools import wraps
def method_wrapper(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        return func(self, *args, **kwargs)
    return wrapper

This behaviour is demonstrated in the example script (both the broken and fixed).

The Example

The demonstrator.py script shows techniques for decorating class methods with arbitrary parameters. The test_demonstrator.py exercises the decorators and highlights some issues and work-arounds.

Running the Example

The output is a bit hard to read, but it provides a detailed log of what happens when calling two decorated methods.

$ ./demonstrator.py
INFO:demonstrator:method_wrapper.before: Demonstrator.apples called with args: ([], 'a message to apples') kwargs: {'example_id': 33}
INFO:demonstrator:running apples with payload: [], message: 'a message to apples', example_id: 33
INFO:demonstrator:method_wrapper.after: returns ['a message to apples', 33]
apples() returned ['a message to apples', 33]
INFO:demonstrator:wrapped_method_wrapper.before: Demonstrator.oranges called with args: ([], 'a message to oranges') kwargs: {'example_id': 33}
INFO:demonstrator:running oranges with payload: [], message: 'a message to oranges', example_id: 33
INFO:demonstrator:wrapped_method_wrapper.after: returns ['a message to oranges', 33]
oranges() returned ['a message to oranges', 33]

Tests

The tests verify the functionality, and also prove the effect of functools.wraps.

The tests use the mock package. If you don’t already have it installed, can install with the requirements.txt and pip:

pip install -r requirements.txt

Running the tests… NB: the expected failures are related to default __name__ and __doc__ behaviour tests.

$ ./test_demonstrator.py
...x..x...
----------------------------------------------------------------------
Ran 10 tests in 0.002s

OK (expected failures=2)

Miscellaneous “Quite Interesting” Details in the Example

How to inhibit logging in unit tests

The ambiguously-named logging.disable actually overrides the log level.

Used in setUp and tearDown, it can be used to temporarily adjust the log level during tests:

def setUp(self):
    logging.disable(logging.CRITICAL)

def tearDown(self):
    logging.disable(logging.NOTSET)

Expecting Failure

The unittest.expectedFailure decorator can be used to mark tests that will fail.

It is used here to demonstrate how the decorator obscures method details. For example the docstring:

@unittest.expectedFailure
def test_apples_doc_is_available(self):
    self.assertEqual(
        ' Doc for apples(). ',
        self.instance.apples.__doc__
    )

Credits and References

About LCK#70 python
Project Source on GitHub Return to the Project Catalog

This page is a web-friendly rendering of my project notes shared in the LittleCodingKata GitHub repository.

LittleCodingKata is my collection of programming exercises, research and code toys broadly spanning things that relate to programming and software development (languages, frameworks and tools).

These range from the trivial to the complex and serious. Many are inspired by existing work and I'll note credits and references where applicable. The focus is quite scattered, as I variously work on things new and important in the moment, or go back to revisit things from the past.

This is primarily a personal collection for my own edification and learning, but anyone who stumbles by is welcome to borrow, steal or reference the work here. And if you spot errors or issues I'd really appreciate some feedback - create an issue, send me an email or even send a pull-request.