mirror of https://github.com/django/django.git
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:
parent
2a5d2eefc7
commit
7f3cfaa12b
1
AUTHORS
1
AUTHORS
|
@ -58,6 +58,7 @@ answer newbie questions, and generally made Django that much better:
|
||||||
Ali Vakilzade <ali@vakilzade.com>
|
Ali Vakilzade <ali@vakilzade.com>
|
||||||
Aljaž Košir <aljazkosir5@gmail.com>
|
Aljaž Košir <aljazkosir5@gmail.com>
|
||||||
Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
|
Aljosa Mohorovic <aljosa.mohorovic@gmail.com>
|
||||||
|
Alokik Vijay <alokik.roe@gmail.com>
|
||||||
Amit Chakradeo <https://amit.chakradeo.net/>
|
Amit Chakradeo <https://amit.chakradeo.net/>
|
||||||
Amit Ramon <amit.ramon@gmail.com>
|
Amit Ramon <amit.ramon@gmail.com>
|
||||||
Amit Upadhyay <http://www.amitu.com/blog/>
|
Amit Upadhyay <http://www.amitu.com/blog/>
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
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,11 +1,15 @@
|
||||||
"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
|
||||||
|
|
||||||
|
@ -239,3 +243,43 @@ 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 get_view_name
|
from .utils import _is_callback, 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,8 +166,7 @@ class ViewDetailView(BaseAdminDocsView):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_view_func(view):
|
def _get_view_func(view):
|
||||||
urlconf = get_urlconf()
|
if _is_callback(view):
|
||||||
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,21 +437,6 @@ 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__(
|
||||||
|
@ -469,9 +454,6 @@ 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()
|
||||||
|
|
||||||
|
@ -545,7 +527,6 @@ 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,
|
||||||
|
@ -604,7 +585,6 @@ 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
|
||||||
|
@ -649,11 +629,6 @@ 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 = []
|
||||||
|
|
|
@ -594,6 +594,10 @@ Miscellaneous
|
||||||
:meth:`~django.db.models.BaseConstraint.validate` method to allow those
|
:meth:`~django.db.models.BaseConstraint.validate` method to allow those
|
||||||
constraints to be used for validation.
|
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:
|
.. _deprecated-features-4.1:
|
||||||
|
|
||||||
Features deprecated in 4.1
|
Features deprecated in 4.1
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
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
|
from .tests import AdminDocsSimpleTestCase, SimpleTestCase
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipUnless(docutils_is_available, "no docutils installed.")
|
@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"
|
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,27 +640,6 @@ 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
|
||||||
|
|
Loading…
Reference in New Issue