Introspecting Python Decorators

Thu 25 August 2011 by Jim Purbrick

Over the last couple of years I’ve found myself using python decorators to annotate handlers for web requests more and more, both when using Django and with micro-frameworks like mnml and newf.

Where the same functionality is required for all handlers, or the required functionality can be determined from standard request or response headers, using WSGI or Django middleware is fine, but where the required functionality is varies based on the handler its much cleaner to use a parameterised decorator than poluting the environment or response objects just to control the middleware. Functionality can be added to a framework as a suite of decorators and plugged together in an aspect oriented way like lego to easily build up sophisticated behaviours.

Unlike other mechanisms for implementing macros, templating or aspect orientation that introduce a new language, python decorators are pure syntactic sugar that under the hood are simply rewritten as python expressions:

@requires_oauth_scope("email")
def notify_friends(request):
    pass

Is simply shorthand for:

def notify_friends(request):
    pass

notify_friends = requires_oauth_scope("email")(notify_friends)

This simplicity is powerful as it allows decorators to also be used as normal functions, for example to build up higher level decorators that bundle common decorator configurations, but it also means that decorators potentially interact badly with another powerful Python feature: introspection.

In the above example the undecorated notify_friends function has the name “notify_friends”, but the decorated function has the name “requires_oauth_scope”. When decorators are used extensively, this can seriously impact the usefulness of introspection for debugging or generating documentation.

Decorating your decorators with the functools @wraps decorator, which copies the name of the wrapped function over to the wrapping function solves this introspection problem, but introduces another: the decorators now become invisible to introspection. In the example above the name of the decorated function would now be “notify_friends” as in the undecorated case, but we wouldn’t know that the function had been decorated or not.

A potential solution to this new problem is to store the details about the decoration in another attribute that can be inspected at runtime. In addition to copying over the name attribute, functools.wraps also copies over the target dict by default, allowing it to be used to store information about the decoration and be correctly copied over when decorators are chained:

from functools import wraps

def requires_oauth_scope(scope):

    def decorator(target):

        target.__dict__["my_project_requires_oauth_scope"] = scope

        @wraps(target)
        def wrapper(*args, **kwargs):
            # return target(*args, **kwargs) or FORBIDDEN if token does not have required scope

        return wrapper
    return decorator

By constructing decorators in this way we get the benefits of python decorators and more declarative C# style attributes that are visible to introspection.


Comments