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


😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸


Subscribe via RSS, Twitter, Mastodon, or email:

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

Related posts:

Tags: