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:

  1. Views from Django (such as the admin views) are tested as part of Django's release, so we can just assume those are safe.
  2. 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.
  3. 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")