Django: All views, labelled by origin
In the ReHome project, we have a self-imposed requirement that all our views
should be decorated with @require_http_methods. I have imposed this
requirement mostly for my own peace of mind, but how do we structure a test to
enforce it?
Get all views
Getting "all views in the current project" is possible, but more annoying than you would expect. To do it, we need to work backwards from the URL patterns and derive the views from there.
Thankfully, we have
a shortcut from django-extensions that does just that,
although it isn't part of a tagged release as of today, 23 Mar 2026. But since
django-extensions is licensed under an MIT License, we can freely reuse the
code and we don't even have to break the law to do it.
The extract_views_from_urlpatterns returns a list of triples of the form:
(<function>, <path>, <name>)
which are sort of unwieldy to work with. We'll wrap them in a proper structure later.
Find out where a view comes from
Here, "origin" means something specific: who is responsible for testing it? Broadly we have three origins in a Django project:
- Views from Django (such as the admin views) are tested as part of Django's release, so we can just assume those are safe.
- Views from external dependencies like django-allauth are tested by their dependency, so we only need to test them in so far as we have modified them or integrated them with our own project.
- Local apps are entirely our responsibility, so we need to test them completely.
So we definitely want to test that @require_http_methods is used on all of our
project views, but we don't care about external or Django views using it. Thus,
we need a way to clearly separate views that originate locally.
The bugbear here is that Django doesn't label the views for us. That means we'll need to do something intelligent in order to link the views back to the app they come from, and then filter out the views that come from apps we don't control.
A Django view is just a function,
even if the view is defined as a class-based view. That means each and every
view in Django is labelled with a __module__ magic string which tells us the
exact module where it was defined. This is the key that will let us crack the
whole thing open:
len.__module__ # 'builtins'
cheeses_index.__module__ # 'materials.cheeses.views'
# etc.
We'll have to deal with a couple .views and miscellaneous sub-modules, but
this is a definitive way to source back to a view's origin given the view
callable, which we can get for every view loaded in the project using our
extract_views_from_urlpatterns.
Label apps with their origins
Figuring out where individual Django apps come from is way easier! Django
settings are just Python constants, so we can just manually label the apps in
the settings.py file as we define them:
# As long as the names are CONSTANT_CASE, these lists will even be loaded
# automatically into the `settings` object for us, so we can easily access
# them anywhere in the project, too!
DJANGO_APPS = [...]
EXTERNAL_APPS = [...]
LOCAL_APPS = [...]
INSTALLED_APPS = DJANGO_APPS + EXTERNAL_APPS + LOCAL_APPS
Structured view metadata
Here's what I ended up with as an object for working views as objects:
class View:
def __init__(self, fn: Callable, path: str, name: str):
self.fn = fn
self.path = path
self.name = name
if "django" in self.fn.__module__:
self.origin = "django"
elif any(
self.fn.__module__.startswith(app)
for app in settings.EXTERNAL_APPS
):
self.origin = "external"
else:
self.origin = "local"
# __str__, etc.
That elif line is worth a closer look: Imagine you have a view like the
account login page from allauth. Then you'll get a __module__ value
allauth.accounts.views. But the actual app name from the settings is
allauth.accounts. So, we can just check if the beginning of the module
matches a known app name.
Putting it all together
A final fixture for Pytest looks like this:
@pytest.fixture
def get_views():
"""
Get a list of structured metadata for all views in the project.
"""
resolver = get_resolver()
all_views = extract_views_from_urlpatterns(resolver.url_patterns)
views = [View(fn=fn, name=name, path=path) for (fn, path, name) in all_views]
def inner(django=False, external=False, local=False):
kwargs = {"django": django, "external": external, "local": local}
return [view for view in views if kwargs[view.origin]]
return inner
The fixture is a higher-order function that follows a sort of factories-as-fixtures pattern.
The culmination of all that work: The actual test
Now we can easily build the actual test we want:
def test_local_views_are_decorated_with_require_http_methods(
get_views,
) -> None:
views = get_views(local=True)
<somehow check that the view has been decorated with require_http_methods>
... Assuming we have an easy way to test that the function has been decorated, which we actually don't. Thankfully, we can fix that easily with a custom variant of the decorator.
Assume we mark the view with an allowed_methods attribute to track the allowed
methods; then we can get our final version of the test:
def test_local_views_are_decorated_with_require_http_methods(
get_views,
) -> None:
views = get_views(local=True)
for view in views:
assert hasattr(view.fn, "allowed_methods")