Fixed #33955, Fixed #33971 -- Reverted "Fixed #32565 -- Moved internal URLResolver view-strings mapping to admindocs."

This reverts commit 7f3cfaa12b.

Thanks Tom Carrick and Greg Kaleka for reports.
This commit is contained in:
Mariusz Felisiak 2022-09-01 21:09:16 +02:00 committed by GitHub
parent e64919ae54
commit 974942a750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 54 additions and 82 deletions

View File

@ -1,15 +1,7 @@
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,15 +1,11 @@
"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
@ -243,43 +239,3 @@ 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 _is_callback, get_view_name
from .utils import get_view_name
# Exclude methods starting with these strings from documentation
MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_")
@ -166,7 +166,8 @@ class ViewDetailView(BaseAdminDocsView):
@staticmethod
def _get_view_func(view):
if _is_callback(view):
urlconf = get_urlconf()
if get_resolver(urlconf)._is_callback(view):
mod, func = get_mod_func(view)
try:
# Separate the module and function, e.g.

View File

@ -437,6 +437,21 @@ 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__(
@ -454,6 +469,9 @@ 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()
@ -527,6 +545,7 @@ 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,
@ -585,6 +604,7 @@ 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
@ -629,6 +649,11 @@ 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

@ -55,3 +55,7 @@ Bugfixes
* Fixed a regression in Django 4.1 that caused a migration crash on SQLite <
3.20 (:ticket:`33960`).
* Fixed a regression in Django 4.1 that caused an admin crash when the
:mod:`~django.contrib.admindocs` app was used (:ticket:`33955`,
:ticket:`33971`).

View File

@ -1,15 +1,13 @@
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, SimpleTestCase
from .tests import AdminDocsSimpleTestCase
@unittest.skipUnless(docutils_is_available, "no docutils installed.")
@ -121,28 +119,3 @@ 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,6 +640,27 @@ 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