Django: hoist repeated decorator definitions

Spiky lizard likes things DRY.

Django provides us with a rich set of view decorators. In this post, we’ll look at a technique for hoisting repeated use of these decorators to reduce repetition.

Repeated @cache_control calls

Here are two public views with the same @cache_control decorator:

from django.views.decorators.cache import cache_control


@cache_control(max_age=60 * 60, public=True)
def about(request): ...


@cache_control(max_age=60 * 60, public=True)
def contact_us(request): ...

To avoid this repetition, we can call cache_control once at the top of the module and use that result as the decorator:

from django.views.decorators.cache import cache_control

cache_public = cache_control(max_age=60 * 60, public=True)


@cache_public
def about(request): ...


@cache_public
def team(request): ...

This works because cache_control is technically not a decorator but a function that returns a decorator. So we can separate the call of cache_control from the decorating.

Aside from reducing redundant repetition, this technique also saves a tiny bit of time and memory when importing the module, because cache_control is only called once.

Repeated @require_http_methods calls

Here’s another example, instead using @require_http_methods:

from django.views.decorators.http import require_http_methods

require_GET_POST = require_http_methods(("GET", "POST"))


@require_GET_POST
def contact_us(request): ...


@require_GET_POST
def store_feedback(request): ...

(Actually, it would be neat if Django provided require_GET_POST out of the box…)

Hoisting @method_decorator calls for class-based views

This technique is particularly beneficial for class-based views, where view decorators mostly need extra wrapping with method_decorator:

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView

cache_public = method_decorator(
    cache_control(max_age=60 * 60, public=True),
    name="dispatch",
)


@cache_public
class AboutView(TemplateView): ...


@cache_public
class TeamView(TemplateView): ...

I also like to use this technique with decorators that don’t take arguments, such as the new @login_not_required from Django 5.1:

from django.contrib.auth.decorators import login_not_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

login_not_required_m = method_decorator(login_not_required, name="dispatch")


@login_not_required_m
class AboutView(TemplateView): ...


@login_not_required_m
class TeamView(TemplateView): ...

I like adding an “m” suffix to the variable name to indicate that it’s a method decorator version of the original.

Test decorators

This deduplication technique can also dramatically improve test readability, where many tests often need the same decorator applied. For example, third-party apps may mark version-restricted tests with unittest’s @skipIf or pytest’s @pytest.mark.skipif:

from unittest import skipIf

import django

django_5_1_plus = skipIf(django.VERSION < (5, 1), "Django 5.1+ required")


class AcmeAuthMiddlewareTests(TestCase):
    ...

    @django_5_1_plus
    def test_view_login_not_required(self): ...

    @django_5_1_plus
    def test_view_login_required(self): ...

Fin

May your decorators be DRYer than the Kalahari,

—Adam


Learn how to make your tests run quickly in my book Speed Up Your Django Tests.


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: