Django: iterate through all registered 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):
...
😸😸😸 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: model field choices that can change without a database migration
- Django: hide the development server warning
- Django: render JavaScript import maps in templates
Tags: django