summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlokik Vijay <alokik.roe@gmail.com>2022-05-17 09:46:26 +0200
committerCarlton Gibson <carlton@noumenal.es>2022-05-17 10:50:09 +0200
commit7f3cfaa12b28d15c0ca78bb692bfd6e59d17bff1 (patch)
treeb9da3508b15e3bbbb42332f282ce8e0075f5d7e0
parent2a5d2eefc751be012fdebd75d8177c42bf5a76fc (diff)
Fixed #32565 -- Moved internal URLResolver view-strings mapping to admindocs.
Moved the functionality of URLResolver._is_callback(), URLResolver._callback_strs, URLPattern.lookup_str() to django.contrib.admindocs.
-rw-r--r--AUTHORS1
-rw-r--r--django/contrib/admindocs/apps.py8
-rw-r--r--django/contrib/admindocs/utils.py44
-rw-r--r--django/contrib/admindocs/views.py5
-rw-r--r--django/urls/resolvers.py25
-rw-r--r--docs/releases/4.1.txt4
-rw-r--r--tests/admin_docs/test_utils.py29
-rw-r--r--tests/urlpatterns_reverse/tests.py21
8 files changed, 87 insertions, 50 deletions
diff --git a/AUTHORS b/AUTHORS
index 3a9a521c19..1f39fdb6c4 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -58,6 +58,7 @@ answer newbie questions, and generally made Django that much better:
Ali Vakilzade <ali@vakilzade.com>
Aljaž Košir <aljazkosir5@gmail.com>
Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
+ Alokik Vijay <alokik.roe@gmail.com>
Amit Chakradeo <https://amit.chakradeo.net/>
Amit Ramon <amit.ramon@gmail.com>
Amit Upadhyay <http://www.amitu.com/blog/>
diff --git a/django/contrib/admindocs/apps.py b/django/contrib/admindocs/apps.py
index e79dc892cb..2fda5fb4fb 100644
--- a/django/contrib/admindocs/apps.py
+++ b/django/contrib/admindocs/apps.py
@@ -1,7 +1,15 @@
from django.apps import AppConfig
+from django.urls import get_resolver, get_urlconf
from django.utils.translation import gettext_lazy as _
+from .utils import _active, register_callback
+
class AdminDocsConfig(AppConfig):
name = "django.contrib.admindocs"
verbose_name = _("Administrative Documentation")
+
+ def ready(self):
+ urlconf = get_urlconf()
+ urlresolver = get_resolver(urlconf)
+ register_callback(urlresolver, _active.local_value)
diff --git a/django/contrib/admindocs/utils.py b/django/contrib/admindocs/utils.py
index 6edff502ec..a18dbe12cd 100644
--- a/django/contrib/admindocs/utils.py
+++ b/django/contrib/admindocs/utils.py
@@ -1,11 +1,15 @@
"Misc. utility functions/classes for admin documentation generator."
+import functools
import re
from email.errors import HeaderParseError
from email.parser import HeaderParser
from inspect import cleandoc
+from asgiref.local import Local
+
from django.urls import reverse
+from django.urls.resolvers import URLPattern
from django.utils.regex_helper import _lazy_re_compile
from django.utils.safestring import mark_safe
@@ -239,3 +243,43 @@ def remove_non_capturing_groups(pattern):
final_pattern += pattern[prev_end:start]
prev_end = end
return final_pattern + pattern[prev_end:]
+
+
+# Callback strings are cached in a dictionary for every urlconf.
+# The active calback_strs are stored by thread id to make them thread local.
+_callback_strs = set()
+_active = Local()
+_active.local_value = _callback_strs
+
+
+def _is_callback(name, urlresolver=None):
+ if urlresolver and not urlresolver._populated:
+ register_callback(urlresolver, _active.local_value)
+ return name in _active.local_value
+
+
+@functools.lru_cache(maxsize=None)
+def lookup_str(urlpattern):
+ """
+ A string that identifies the view (e.g. 'path.to.view_function' or
+ 'path.to.ClassBasedView').
+ """
+ callback = urlpattern.callback
+ if isinstance(callback, functools.partial):
+ callback = callback.func
+ if hasattr(callback, "view_class"):
+ callback = callback.view_class
+ elif not hasattr(callback, "__name__"):
+ return callback.__module__ + "." + callback.__class__.__name__
+ return callback.__module__ + "." + callback.__qualname__
+
+
+def register_callback(urlresolver, thread):
+ for url_pattern in reversed(urlresolver.url_patterns):
+ if isinstance(url_pattern, URLPattern):
+ thread.add(lookup_str(url_pattern))
+ else: # url_pattern is a URLResolver.
+ _active.url_pattern_value = _callback_strs
+ register_callback(url_pattern, _active.url_pattern_value)
+ thread.update(_active.url_pattern_value)
+ urlresolver._populated = True
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 1d08541057..f771e8f391 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -30,7 +30,7 @@ from django.utils.inspect import (
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
-from .utils import get_view_name
+from .utils import _is_callback, get_view_name
# Exclude methods starting with these strings from documentation
MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_")
@@ -166,8 +166,7 @@ class ViewDetailView(BaseAdminDocsView):
@staticmethod
def _get_view_func(view):
- urlconf = get_urlconf()
- if get_resolver(urlconf)._is_callback(view):
+ if _is_callback(view):
mod, func = get_mod_func(view)
try:
# Separate the module and function, e.g.
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 9f42e2738c..7c0d4fd85a 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -437,21 +437,6 @@ class URLPattern:
extra_kwargs=self.default_args,
)
- @cached_property
- def lookup_str(self):
- """
- A string that identifies the view (e.g. 'path.to.view_function' or
- 'path.to.ClassBasedView').
- """
- callback = self.callback
- if isinstance(callback, functools.partial):
- callback = callback.func
- if hasattr(callback, "view_class"):
- callback = callback.view_class
- elif not hasattr(callback, "__name__"):
- return callback.__module__ + "." + callback.__class__.__name__
- return callback.__module__ + "." + callback.__qualname__
-
class URLResolver:
def __init__(
@@ -469,9 +454,6 @@ class URLResolver:
self._reverse_dict = {}
self._namespace_dict = {}
self._app_dict = {}
- # set of dotted paths to all functions and classes that are used in
- # urlpatterns
- self._callback_strs = set()
self._populated = False
self._local = Local()
@@ -545,7 +527,6 @@ class URLResolver:
if p_pattern.startswith("^"):
p_pattern = p_pattern[1:]
if isinstance(url_pattern, URLPattern):
- self._callback_strs.add(url_pattern.lookup_str)
bits = normalize(url_pattern.pattern.regex.pattern)
lookups.appendlist(
url_pattern.callback,
@@ -604,7 +585,6 @@ class URLResolver:
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
for app_name, namespace_list in url_pattern.app_dict.items():
apps.setdefault(app_name, []).extend(namespace_list)
- self._callback_strs.update(url_pattern._callback_strs)
self._namespace_dict[language_code] = namespaces
self._app_dict[language_code] = apps
self._reverse_dict[language_code] = lookups
@@ -649,11 +629,6 @@ class URLResolver:
route2 = route2[1:]
return route1 + route2
- def _is_callback(self, name):
- if not self._populated:
- self._populate()
- return name in self._callback_strs
-
def resolve(self, path):
path = str(path) # path may be a reverse_lazy object
tried = []
diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt
index bd8af7d51c..600da9dea9 100644
--- a/docs/releases/4.1.txt
+++ b/docs/releases/4.1.txt
@@ -594,6 +594,10 @@ Miscellaneous
:meth:`~django.db.models.BaseConstraint.validate` method to allow those
constraints to be used for validation.
+* The undocumented ``URLResolver._is_callback()``,
+ ``URLResolver._callback_strs``, and ``URLPattern.lookup_str()`` have been
+ moved to ``django.contrib.admindocs.utils``.
+
.. _deprecated-features-4.1:
Features deprecated in 4.1
diff --git a/tests/admin_docs/test_utils.py b/tests/admin_docs/test_utils.py
index 18c6769fad..e26a4ca1f3 100644
--- a/tests/admin_docs/test_utils.py
+++ b/tests/admin_docs/test_utils.py
@@ -1,13 +1,15 @@
import unittest
from django.contrib.admindocs.utils import (
+ _is_callback,
docutils_is_available,
parse_docstring,
parse_rst,
)
from django.test.utils import captured_stderr
+from django.urls import get_resolver
-from .tests import AdminDocsSimpleTestCase
+from .tests import AdminDocsSimpleTestCase, SimpleTestCase
@unittest.skipUnless(docutils_is_available, "no docutils installed.")
@@ -119,3 +121,28 @@ class TestUtils(AdminDocsSimpleTestCase):
markup = "<p>reST, <cite>interpreted text</cite>, default role.</p>\n"
parts = docutils.core.publish_parts(source=source, writer_name="html4css1")
self.assertEqual(parts["fragment"], markup)
+
+
+class TestResolver(SimpleTestCase):
+ def test_namespaced_view_detail(self):
+ resolver = get_resolver("urlpatterns_reverse.nested_urls")
+ self.assertTrue(_is_callback("urlpatterns_reverse.nested_urls.view1", resolver))
+ self.assertTrue(_is_callback("urlpatterns_reverse.nested_urls.view2", resolver))
+ self.assertTrue(_is_callback("urlpatterns_reverse.nested_urls.View3", resolver))
+ self.assertFalse(_is_callback("urlpatterns_reverse.nested_urls.blub", resolver))
+
+ def test_view_detail_as_method(self):
+ # Views which have a class name as part of their path.
+ resolver = get_resolver("urlpatterns_reverse.method_view_urls")
+ self.assertTrue(
+ _is_callback(
+ "urlpatterns_reverse.method_view_urls.ViewContainer.method_view",
+ resolver,
+ )
+ )
+ self.assertTrue(
+ _is_callback(
+ "urlpatterns_reverse.method_view_urls.ViewContainer.classmethod_view",
+ resolver,
+ )
+ )
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index 89dfd0deba..73159b6bb8 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -640,27 +640,6 @@ class ResolverTests(SimpleTestCase):
% (e["name"], t.name),
)
- def test_namespaced_view_detail(self):
- resolver = get_resolver("urlpatterns_reverse.nested_urls")
- self.assertTrue(resolver._is_callback("urlpatterns_reverse.nested_urls.view1"))
- self.assertTrue(resolver._is_callback("urlpatterns_reverse.nested_urls.view2"))
- self.assertTrue(resolver._is_callback("urlpatterns_reverse.nested_urls.View3"))
- self.assertFalse(resolver._is_callback("urlpatterns_reverse.nested_urls.blub"))
-
- def test_view_detail_as_method(self):
- # Views which have a class name as part of their path.
- resolver = get_resolver("urlpatterns_reverse.method_view_urls")
- self.assertTrue(
- resolver._is_callback(
- "urlpatterns_reverse.method_view_urls.ViewContainer.method_view"
- )
- )
- self.assertTrue(
- resolver._is_callback(
- "urlpatterns_reverse.method_view_urls.ViewContainer.classmethod_view"
- )
- )
-
def test_populate_concurrency(self):
"""
URLResolver._populate() can be called concurrently, but not more