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.
This commit is contained in:
Alokik Vijay 2022-05-17 09:46:26 +02:00 committed by Carlton Gibson
parent 2a5d2eefc7
commit 7f3cfaa12b
8 changed files with 87 additions and 50 deletions

View File

@ -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/>

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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 = []

View File

@ -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

View File

@ -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,
)
)

View File

@ -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