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.apps import AppConfig | ||||||
| from django.urls import get_resolver, get_urlconf |  | ||||||
| from django.utils.translation import gettext_lazy as _ | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
| from .utils import _active, register_callback |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AdminDocsConfig(AppConfig): | class AdminDocsConfig(AppConfig): | ||||||
|     name = "django.contrib.admindocs" |     name = "django.contrib.admindocs" | ||||||
|     verbose_name = _("Administrative Documentation") |     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." | "Misc. utility functions/classes for admin documentation generator." | ||||||
|  |  | ||||||
| import functools |  | ||||||
| import re | import re | ||||||
| from email.errors import HeaderParseError | from email.errors import HeaderParseError | ||||||
| from email.parser import HeaderParser | from email.parser import HeaderParser | ||||||
| from inspect import cleandoc | from inspect import cleandoc | ||||||
|  |  | ||||||
| from asgiref.local import Local |  | ||||||
|  |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.urls.resolvers import URLPattern |  | ||||||
| from django.utils.regex_helper import _lazy_re_compile | from django.utils.regex_helper import _lazy_re_compile | ||||||
| from django.utils.safestring import mark_safe | from django.utils.safestring import mark_safe | ||||||
|  |  | ||||||
| @@ -243,43 +239,3 @@ def remove_non_capturing_groups(pattern): | |||||||
|         final_pattern += pattern[prev_end:start] |         final_pattern += pattern[prev_end:start] | ||||||
|         prev_end = end |         prev_end = end | ||||||
|     return final_pattern + pattern[prev_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.utils.translation import gettext as _ | ||||||
| from django.views.generic import TemplateView | 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 | # Exclude methods starting with these strings from documentation | ||||||
| MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_") | MODEL_METHODS_EXCLUDE = ("_", "add_", "delete", "save", "set_") | ||||||
| @@ -166,7 +166,8 @@ class ViewDetailView(BaseAdminDocsView): | |||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _get_view_func(view): |     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) |             mod, func = get_mod_func(view) | ||||||
|             try: |             try: | ||||||
|                 # Separate the module and function, e.g. |                 # Separate the module and function, e.g. | ||||||
|   | |||||||
| @@ -437,6 +437,21 @@ class URLPattern: | |||||||
|                 extra_kwargs=self.default_args, |                 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: | class URLResolver: | ||||||
|     def __init__( |     def __init__( | ||||||
| @@ -454,6 +469,9 @@ class URLResolver: | |||||||
|         self._reverse_dict = {} |         self._reverse_dict = {} | ||||||
|         self._namespace_dict = {} |         self._namespace_dict = {} | ||||||
|         self._app_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._populated = False | ||||||
|         self._local = Local() |         self._local = Local() | ||||||
|  |  | ||||||
| @@ -527,6 +545,7 @@ class URLResolver: | |||||||
|                 if p_pattern.startswith("^"): |                 if p_pattern.startswith("^"): | ||||||
|                     p_pattern = p_pattern[1:] |                     p_pattern = p_pattern[1:] | ||||||
|                 if isinstance(url_pattern, URLPattern): |                 if isinstance(url_pattern, URLPattern): | ||||||
|  |                     self._callback_strs.add(url_pattern.lookup_str) | ||||||
|                     bits = normalize(url_pattern.pattern.regex.pattern) |                     bits = normalize(url_pattern.pattern.regex.pattern) | ||||||
|                     lookups.appendlist( |                     lookups.appendlist( | ||||||
|                         url_pattern.callback, |                         url_pattern.callback, | ||||||
| @@ -585,6 +604,7 @@ class URLResolver: | |||||||
|                             namespaces[namespace] = (p_pattern + prefix, sub_pattern) |                             namespaces[namespace] = (p_pattern + prefix, sub_pattern) | ||||||
|                         for app_name, namespace_list in url_pattern.app_dict.items(): |                         for app_name, namespace_list in url_pattern.app_dict.items(): | ||||||
|                             apps.setdefault(app_name, []).extend(namespace_list) |                             apps.setdefault(app_name, []).extend(namespace_list) | ||||||
|  |                     self._callback_strs.update(url_pattern._callback_strs) | ||||||
|             self._namespace_dict[language_code] = namespaces |             self._namespace_dict[language_code] = namespaces | ||||||
|             self._app_dict[language_code] = apps |             self._app_dict[language_code] = apps | ||||||
|             self._reverse_dict[language_code] = lookups |             self._reverse_dict[language_code] = lookups | ||||||
| @@ -629,6 +649,11 @@ class URLResolver: | |||||||
|             route2 = route2[1:] |             route2 = route2[1:] | ||||||
|         return route1 + route2 |         return route1 + route2 | ||||||
|  |  | ||||||
|  |     def _is_callback(self, name): | ||||||
|  |         if not self._populated: | ||||||
|  |             self._populate() | ||||||
|  |         return name in self._callback_strs | ||||||
|  |  | ||||||
|     def resolve(self, path): |     def resolve(self, path): | ||||||
|         path = str(path)  # path may be a reverse_lazy object |         path = str(path)  # path may be a reverse_lazy object | ||||||
|         tried = [] |         tried = [] | ||||||
|   | |||||||
| @@ -55,3 +55,7 @@ Bugfixes | |||||||
|  |  | ||||||
| * Fixed a regression in Django 4.1 that caused a migration crash on SQLite < | * Fixed a regression in Django 4.1 that caused a migration crash on SQLite < | ||||||
|   3.20 (:ticket:`33960`). |   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 | import unittest | ||||||
|  |  | ||||||
| from django.contrib.admindocs.utils import ( | from django.contrib.admindocs.utils import ( | ||||||
|     _is_callback, |  | ||||||
|     docutils_is_available, |     docutils_is_available, | ||||||
|     parse_docstring, |     parse_docstring, | ||||||
|     parse_rst, |     parse_rst, | ||||||
| ) | ) | ||||||
| from django.test.utils import captured_stderr | 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.") | @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" |         markup = "<p>reST, <cite>interpreted text</cite>, default role.</p>\n" | ||||||
|         parts = docutils.core.publish_parts(source=source, writer_name="html4css1") |         parts = docutils.core.publish_parts(source=source, writer_name="html4css1") | ||||||
|         self.assertEqual(parts["fragment"], markup) |         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), |                                 % (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): |     def test_populate_concurrency(self): | ||||||
|         """ |         """ | ||||||
|         URLResolver._populate() can be called concurrently, but not more |         URLResolver._populate() can be called concurrently, but not more | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user