Wrapping views with decorators

Published:

Django provides many decorators for use throughout your project. They can be great time savers, and mastering them can help you DRY your code considerable. However, they often confuse people as to how they work, or how to write their own.

In this post I plan to walk through an example of building up a simple decorator that tests if the user has a specific permission, and if not returning a 403 Forbidden response.

Decorator basics.

The decorator syntax does not add anything you can't already do in other ways.

def myview(...):
    ...

myview = mydecorator(myview)

is equivalent to:

@mydecorator
def myview(...):
    ...

That's right - a decorator just calls your decorator function, passing your function [or class] as its only argument, and assigning the result to the same name.

Common Code

So, in this scenario you would find yourself starting a lot of views like this:

def myview(request, ...):
    if not request.user.has_perm('foo.bar'):
        return http.HttpResponseForbidden()
    ...

Repeating this code is obviously tedious and hazardous. Tedious because you're repeating yourself, and hazardous because you may update the permission name in one place, but not others.

Step 1: Fixed permission

First, we'll write a decorator that will do our permission test, but with a hard-coded permission.

def require_permission(view):

    def _inner(request, *args, **kwargs):
        if not request.user.has_perm('foo.bar'):
            return http.HttpResponseForbidden()
        return view(request, *args, **kwargs)

    return _inner

So what just happened there? We defined a new function which implements the permission check, and calls our view if it's ok, returning its result. Any JavaScript developers will easily recognise this pattern.

The fact it can access the 'view' argument that was never passed to it is a factor of Python scoping.

We can use it as simply as:

@require_permission
def myview(request, ....):
    ...

Which is equivalent, as mentioned, to:

def myview(request, ...):
    ...

myview = require_permission(myview)

So, what's returned from require_permission [and assigned to myview] is the wrapper function defined in the decorator.

Step 2: Variable permission

But now we want to choose which permission to test for. Your first thought may be to just pass another argument with the view, but that doesn't work with decorators - it's Python invoking the call, and it only passes one argument: the function.

So what we need is a way to produce new decorator functions, differing on which permission they test. We need a decorator factory.

def require_permission(perm):

    def _outer(view):
        def _inner(request, *args, **kwargs):
            if not request.user.has_perm(perm):
                return http.HttpResponseForbidden()
            return view(request, *args, **kwargs)
        return _inner

    return _outer

Holy Inception, Batman!

So, now we have a function which returns a function, which returns a function, which calls our view!

So what happens here is:

  1. require_permission('foo.bar') calls our factory function, which returns _outer [the actual decorator]
  2. Python calls our decorator [_outer], passing the view as its argument
  3. Our _outer function then defines a new view function which implements our permission check.

Step 3: Tidying up

There is one final step to be made. It's not obvious, but currently if, for whatever reason, the decorator raises an exception, you won't know which decorated function raised the exception. Fortunately, Python has a solution in functools: wraps.

from functools import wraps

def require_permission(perm):

    def _outer(view):

        @wraps(view)
        def _inner(request, *args, **kwargs):
            if not request.user.has_perm(perm):
                return http.HttpResponseForbidden()
            return view(request, *args, **kwargs)
        return _inner

    return _outer

The "@wraps" decorator copies the name, module, and doc properties from the wrapped target, onto the function being decorated. For more details, see functools.wraps

Finally

Of course, there's no reason you can only call decorators using @, since they're just functions. And so is our decorator factory. So, coming back to the idea of DRYing out code, we can pre-generate decorators for specific permissions:

require_foo_permission = require_permission('myapp.foo')
require_bar_permission = require_permission('myapp.bar')

@require_foo_permission
def myview(request, ...):

So now we've removed repetition of the same 'how to test for a permission' code, as well as avoided repeating the name of specific permissions.

Further reading

The incomparable Graham Dumpleton has written an extensive series of posts on doing decorators properly, so I highly recommend reading them yourself.