浏览代码

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.
Alokik Vijay 2 年之前
父节点
当前提交
7f3cfaa12b

+ 1 - 0
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/>

+ 8 - 0
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)

+ 44 - 0
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

+ 2 - 3
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.

+ 0 - 25
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 = []

+ 4 - 0
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

+ 28 - 1
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,
+            )
+        )

+ 0 - 21
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