mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[4.1.x] Fixed #33955, Fixed #33971 -- Reverted "Fixed #32565 -- Moved internal URLResolver view-strings mapping to admindocs."
This reverts commit7f3cfaa12b. Thanks Tom Carrick and Greg Kaleka for reports. Backport of974942a750from main
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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 = [] | ||||
|   | ||||
| @@ -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`). | ||||
|   | ||||
| @@ -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, | ||||
|             ) | ||||
|         ) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user