Django: iterate through all registered URL patterns

Let’s horse about with URL patterns!

I’ve found it useful, on occasion, to iterate through all registered URL patterns in a Django project. Sometimes this has been for checking URL layouts or auditing which views are registered.

In this post, we’ll look at a pattern for doing that, along with an example use case.

Get all URL patterns with a recursive generator

The below snippet contains a generator function that traverses Django’s URLResolver structure, which is the parsed representation of your URLconf. It extracts all URLPattern objects, which represent individual URL patterns from path() or re_path() calls, and returns them along with their containing namespace. It handles nested URL resolvers from include() calls by calling itself recursively.

from collections.abc import Generator

from django.urls import URLPattern, URLResolver, get_resolver


def all_url_patterns(
    url_patterns: list | None = None, namespace: str = ""
) -> Generator[tuple[URLPattern, str]]:
    """
    Yield tuples of (URLPattern, namespace) for all URLPattern objects in the
    given Django URLconf, or the default one if none is provided.
    """
    if url_patterns is None:
        url_patterns = get_resolver().url_patterns

    for pattern in url_patterns:
        if isinstance(pattern, URLPattern):
            yield pattern, namespace
        elif isinstance(pattern, URLResolver):
            if pattern.namespace:
                if namespace:
                    namespace = f"{namespace}:{pattern.namespace}"
                else:
                    namespace = pattern.namespace
            yield from all_url_patterns(pattern.url_patterns, namespace)
        else:
            raise TypeError(f"Unexpected pattern type: {type(pattern)} in {namespace}")

An example: finding all class-based views

The below example uses all_url_patterns() to find all registered view classes. The key here is that View.as_view() returns a function, but it attaches a view_class attribute to that function, which points to the actual class.

from example.utils import all_url_patterns

for pattern, namespace in all_url_patterns():
    if hasattr(pattern.callback, "view_class"):
        view_class = pattern.callback.view_class
        print(view_class)

Example output:

<class 'example.views.AboutView'>
<class 'example.views.CashewView'>
<class 'example.views.HazelnutView'>
<class 'example.views.MacadamiaView'>

Test for a given base view class

One use case for this tool is to ensure that all views in your project inherit from a specific base class. This can be useful, for example, to ensure that your custom access control logic is always used. Below is a test case that checks all registered views and fails if any do not inherit from example.views.BaseView.

from inspect import getsourcefile, getsourcelines

from django.test import TestCase

from example.utils import all_url_patterns
from example.views import BaseView


class BaseViewTests(TestCase):
    def test_all_views_inherit_from_base_view(self) -> None:
        non_compliant_view_classes = []

        for url_pattern, namespace in all_url_patterns():
            try:
                view_class = url_pattern.callback.view_class
            except AttributeError:
                # Function-based view
                continue

            if not issubclass(view_class, BaseView):
                non_compliant_view_classes.append(view_class)

        if non_compliant_view_classes:
            error_msg = "Views not inheriting from BaseView:\n"
            for view_class in non_compliant_view_classes:
                file = getsourcefile(view_class)
                _, lineno = getsourcelines(view_class)
                error_msg += f"  - {view_class.__module__}.{view_class.__name__} (defined in {file}:{lineno})\n"
            self.fail(error_msg)

The test finds all class-based views as before, and places the non-compliant ones into the non_compliant_view_classes list. At the end, it raises an error with a detailed message listing the non-compliant view classes, including their “line number path”, per my previous post, which allows quick navigation to the source code.

Running it can produce a failure like:

./manage.py test
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_all_views_inherit_from_base_view (tests.BaseViewTests.test_all_views_inherit_from_base_view)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/.../tests.py", line 29, in test_all_views_inherit_from_base_view
    self.fail(error_msg)
    ~~~~~~~~~^^^^^^^^^^^
AssertionError: Views not inheriting from BaseView:
  - example.views.MacadamiaView (defined in /.../example/views.py:22)


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
Destroying test database for alias 'default'...

In a real project, you might want to extend this test to allow-list certain base classes, such as those used by third-party apps.

(The above example might actually be better as a Django system check, now I think about it.)

Analyze other URLconfs

Some projects use multiple URLconf modules, for example, when hosting multiple domains. To use all_url_patterns() with a specific URLconf, call Django’s get_resolver() with the desired URLconf module name, then pass the returned resolver's url_patterns attribute to all_url_patterns():

from django.urls import get_resolver

from example.utils import all_url_patterns

url_patterns = get_resolver("example.www_urls").url_patterns
for pattern, namespace in all_url_patterns(url_patterns):
    ...

Fin

URL be alright!

—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: