From 77df93b5b1a12620df2d100191edbc98100c1d58 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Sun, 9 Nov 2025 18:51:41 +0000 Subject: [PATCH] refactor: do not call URLResolver._populate() if not needed --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- requirements-dev.txt | 2 -- src/django_components/extension.py | 26 ++++++++++++++++++-------- src/django_components/util/testing.py | 6 ++++++ tests/test_extension.py | 7 +++++++ 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5f76b0..1558cd3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Release notes +## v0.143.1 + +#### Fix + +- Make django-component's position in Django's `INSTALLED_APPS` more lenient by not calling Django's `URLResolver._populate()` if `URLResolver` hasn't been resolved before ([See thread](https://discord.com/channels/1417824875023700000/1417825089675853906/1437034834118840411)). + ## v0.143.0 #### Feat diff --git a/pyproject.toml b/pyproject.toml index 4f288da0..c7458f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "django_components" -version = "0.143.0" +version = "0.143.1" requires-python = ">=3.8, <4.0" description = "A way to create simple reusable template components in Django." keywords = ["django", "components", "css", "js", "html"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 01c86bfe..fa73e8e8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,8 +12,6 @@ asv==0.6.5 # via -r requirements-dev.in asv-runner==0.2.1 # via asv -backports-asyncio-runner==1.2.0 - # via pytest-asyncio build==1.3.0 # via asv cachetools==6.2.0 diff --git a/src/django_components/extension.py b/src/django_components/extension.py index fa1c6324..55ad43b5 100644 --- a/src/django_components/extension.py +++ b/src/django_components/extension.py @@ -1163,9 +1163,7 @@ class ExtensionManager: extensions_url_resolver.url_patterns = urls # Rebuild URL resolver cache to be able to resolve the new routes by their names. - urlconf = get_urlconf() - resolver = get_resolver(urlconf) - resolver._populate() + self._lazy_populate_resolver() # Flush stored events # @@ -1190,6 +1188,22 @@ class ExtensionManager: getattr(self, hook)(data) self._events = [] + # Django processes the paths from `urlpatterns` only once. + # This is at conflict with how we handle URL paths introduced by extensions, + # which may happen AFTER Django processes `urlpatterns`. + # If that happens, we need to force Django to re-process `urlpatterns`. + # If we don't do it, then the new paths added by our extensions won't work + # with e.g. `django.url.reverse()`. + # See https://discord.com/channels/1417824875023700000/1417825089675853906/1437034834118840411 + def _lazy_populate_resolver(self) -> None: + urlconf = get_urlconf() + root_resolver = get_resolver(urlconf) + # However, if Django has NOT yet processed the `urlpatterns`, then do nothing. + # If we called `_populate()` in such case, we may break people's projects + # as the values may be resolved prematurely, before all the needed code is loaded. + if root_resolver._populated: + root_resolver._populate() + def get_extension(self, name: str) -> ComponentExtension: for extension in self.extensions: if extension.name == name: @@ -1221,12 +1235,8 @@ class ExtensionManager: all_urls.append(urlpattern) did_add_urls = True - # Force Django's URLResolver to update its lookups, so things like `reverse()` work if did_add_urls: - # Django's root URLResolver - urlconf = get_urlconf() - root_resolver = get_resolver(urlconf) - root_resolver._populate() + self._lazy_populate_resolver() def remove_extension_urls(self, name: str, urls: List[URLRoute]) -> None: if not self._initialized: diff --git a/src/django_components/util/testing.py b/src/django_components/util/testing.py index e26e7a3e..025c8ca3 100644 --- a/src/django_components/util/testing.py +++ b/src/django_components/util/testing.py @@ -585,5 +585,11 @@ def _clear_djc_global_state( if gc_collect: gc.collect() + # Clear Django's URL resolver cache, so that any URLs that were added + # during tests are removed. + from django.urls.resolvers import _get_cached_resolver # noqa: PLC0415 + + _get_cached_resolver.cache_clear() + global IS_TESTING # noqa: PLW0603 IS_TESTING = False diff --git a/tests/test_extension.py b/tests/test_extension.py index 25de1519..0c48f0d9 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -5,6 +5,7 @@ import pytest from django.http import HttpRequest, HttpResponse from django.template import Context, Origin, Template from django.test import Client +from django.urls import get_resolver, get_urlconf from django_components import Component, Slot, SlotNode, register, registry from django_components.app_settings import app_settings @@ -649,6 +650,12 @@ class TestExtensionHooks: @djc_test class TestExtensionViews: + @djc_test(components_settings={"extensions": [DummyExtension]}) + def test_resolver_not_populated_needlessly(self): + urlconf = get_urlconf() + resolver = get_resolver(urlconf) + assert not resolver._populated + @djc_test(components_settings={"extensions": [DummyExtension]}) def test_views(self): client = Client()