Adam Johnson

Home | Blog | Training | Projects | Colophon | Contact

How to Combine Two Python Decorators

2020-04-01

Imagine you have some Django views using the same two decorators:

from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_GET


@require_GET
@login_required
def home(request):
    ...

@require_GET
@login_required
def about(request):
    ...


@require_GET
@login_required
def contact(request):
    ...

It’s a bit repetitive, and prone to mistakes on new views, such as using the wrong order, or missing one. You can instead combine them into one decorator.

In Python, the decorator syntax is a shortcut for calling the decorator and storing its result in the same name. For example, this snippet:

@login_required
def index(request):
    ...

is the same as:

def index(request):
    ...

index = login_required(index)

Therefore to combine these decorators, you can implement a new function that calls each of the decorators in turn like this. To maintain the same order, you should call them “from the inside out”. So you can make a combined decorator function like this:

def require_GET_and_login(func):
    return require_GET(login_required(func))

You can then apply it to views like this:

@require_GET_and_login
def home(request):
    ...

@require_GET_and_login
def about(request):
    ...


@require_GET_and_login
def contact(request):
    ...

Great!

Some decorators take arguments, for which this technique needs expanding a bit. For example, Django’s require_http_methods is a general version of require_GET that takes an argument for the methods. If you had some repetition using it:

from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required

@require_http_methods(['GET', 'POST'])
@login_required
def contact(request):
    ...


@require_http_methods(['GET', 'POST'])
@login_required
def guestbook(request):
    ...

Then, you create a similar combination decorator, calling require_http_methods with its arguments in the same way:

def require_GET_or_POST_and_login(func):
    return require_http_methods(['GET', 'POST'])(
        login_required(func)
    )

Finally, you could expand the combined decorator to pass arguments through. This means the outer function, which takes the arguments, needs to return an inner decorator function that will be applied to the target. For example, to allow configuring the required methods and whether a login is required in a single decorator:

def require(methods=('GET', 'POST'), login=True):
    def decorator(func):
        wrapped = func
        if methods is not None:
            wrapped = require_http_methods(methods)(func)
        if login:
            wrapped = login_required(func)
        return wrapped
    return decorator

You can then apply this like:

@require(methods=['GET'], login=True)
def index(request):
    ...


@require(methods=['GET', 'POST'], login=False)
def blog(request):
    ...

Fin

I hope this helps you DRY out your decorators,

Adam


Are your Django project's tests slow? Read Speed Up Your Django Tests now!


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: django, python