refactor: do not call URLResolver._populate() if not needed

This commit is contained in:
Juro Oravec 2025-11-09 18:51:41 +00:00
parent 39e4d78dc5
commit 77df93b5b1
6 changed files with 38 additions and 11 deletions

View file

@ -1,5 +1,11 @@
# Release notes # 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 ## v0.143.0
#### Feat #### Feat

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "django_components" name = "django_components"
version = "0.143.0" version = "0.143.1"
requires-python = ">=3.8, <4.0" requires-python = ">=3.8, <4.0"
description = "A way to create simple reusable template components in Django." description = "A way to create simple reusable template components in Django."
keywords = ["django", "components", "css", "js", "html"] keywords = ["django", "components", "css", "js", "html"]

View file

@ -12,8 +12,6 @@ asv==0.6.5
# via -r requirements-dev.in # via -r requirements-dev.in
asv-runner==0.2.1 asv-runner==0.2.1
# via asv # via asv
backports-asyncio-runner==1.2.0
# via pytest-asyncio
build==1.3.0 build==1.3.0
# via asv # via asv
cachetools==6.2.0 cachetools==6.2.0

View file

@ -1163,9 +1163,7 @@ class ExtensionManager:
extensions_url_resolver.url_patterns = urls extensions_url_resolver.url_patterns = urls
# Rebuild URL resolver cache to be able to resolve the new routes by their names. # Rebuild URL resolver cache to be able to resolve the new routes by their names.
urlconf = get_urlconf() self._lazy_populate_resolver()
resolver = get_resolver(urlconf)
resolver._populate()
# Flush stored events # Flush stored events
# #
@ -1190,6 +1188,22 @@ class ExtensionManager:
getattr(self, hook)(data) getattr(self, hook)(data)
self._events = [] 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: def get_extension(self, name: str) -> ComponentExtension:
for extension in self.extensions: for extension in self.extensions:
if extension.name == name: if extension.name == name:
@ -1221,12 +1235,8 @@ class ExtensionManager:
all_urls.append(urlpattern) all_urls.append(urlpattern)
did_add_urls = True did_add_urls = True
# Force Django's URLResolver to update its lookups, so things like `reverse()` work
if did_add_urls: if did_add_urls:
# Django's root URLResolver self._lazy_populate_resolver()
urlconf = get_urlconf()
root_resolver = get_resolver(urlconf)
root_resolver._populate()
def remove_extension_urls(self, name: str, urls: List[URLRoute]) -> None: def remove_extension_urls(self, name: str, urls: List[URLRoute]) -> None:
if not self._initialized: if not self._initialized:

View file

@ -585,5 +585,11 @@ def _clear_djc_global_state(
if gc_collect: if gc_collect:
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 global IS_TESTING # noqa: PLW0603
IS_TESTING = False IS_TESTING = False

View file

@ -5,6 +5,7 @@ import pytest
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.template import Context, Origin, Template from django.template import Context, Origin, Template
from django.test import Client 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 import Component, Slot, SlotNode, register, registry
from django_components.app_settings import app_settings from django_components.app_settings import app_settings
@ -649,6 +650,12 @@ class TestExtensionHooks:
@djc_test @djc_test
class TestExtensionViews: 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]}) @djc_test(components_settings={"extensions": [DummyExtension]})
def test_views(self): def test_views(self):
client = Client() client = Client()