Django: hoist repeated decorator definitions

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): ...
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts:
- Django: avoid “useless use of
.all()” - Django: Join the community on Mastodon
- PostgreSQL: Full text search with the “websearch” syntax
Tags: django