diff --git a/django/urls/converters.py b/django/urls/converters.py index 9b4443058047..b36cde1497b0 100644 --- a/django/urls/converters.py +++ b/django/urls/converters.py @@ -68,11 +68,11 @@ def register_converter(converter, type_name): REGISTERED_CONVERTERS[type_name] = converter() get_converters.cache_clear() + from django.urls.resolvers import _route_to_regex + + _route_to_regex.cache_clear() + @functools.cache def get_converters(): return {**DEFAULT_CONVERTERS, **REGISTERED_CONVERTERS} - - -def get_converter(raw_converter): - return get_converters()[raw_converter] diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py index 1b26aed8c112..c667d7f26871 100644 --- a/django/urls/resolvers.py +++ b/django/urls/resolvers.py @@ -26,7 +26,7 @@ from django.utils.regex_helper import _lazy_re_compile, normalize from django.utils.translation import get_language -from .converters import get_converter +from .converters import get_converters from .exceptions import NoReverseMatch, Resolver404 from .utils import get_callable @@ -243,7 +243,10 @@ def __str__(self): r"<(?:(?P[^>:]+):)?(?P[^>]+)>" ) +whitespace_set = frozenset(string.whitespace) + +@functools.lru_cache def _route_to_regex(route, is_endpoint): """ Convert a path pattern into a regular expression. Return the regular @@ -251,40 +254,37 @@ def _route_to_regex(route, is_endpoint): For example, 'foo/' returns '^foo\\/(?P[0-9]+)' and {'pk': }. """ - original_route = route parts = ["^"] + all_converters = get_converters() converters = {} - while True: - match = _PATH_PARAMETER_COMPONENT_RE.search(route) - if not match: - parts.append(re.escape(route)) - break - elif not set(match.group()).isdisjoint(string.whitespace): + previous_end = 0 + for match_ in _PATH_PARAMETER_COMPONENT_RE.finditer(route): + if not whitespace_set.isdisjoint(match_[0]): raise ImproperlyConfigured( - "URL route '%s' cannot contain whitespace in angle brackets " - "<…>." % original_route + f"URL route {route!r} cannot contain whitespace in angle brackets <…>." ) - parts.append(re.escape(route[: match.start()])) - route = route[match.end() :] - parameter = match["parameter"] + # Default to make converter "str" if unspecified (parameter always + # matches something). + raw_converter, parameter = match_.groups(default="str") if not parameter.isidentifier(): raise ImproperlyConfigured( - "URL route '%s' uses parameter name %r which isn't a valid " - "Python identifier." % (original_route, parameter) + f"URL route {route!r} uses parameter name {parameter!r} which " + "isn't a valid Python identifier." ) - raw_converter = match["converter"] - if raw_converter is None: - # If a converter isn't specified, the default is `str`. - raw_converter = "str" try: - converter = get_converter(raw_converter) + converter = all_converters[raw_converter] except KeyError as e: raise ImproperlyConfigured( - "URL route %r uses invalid converter %r." - % (original_route, raw_converter) + f"URL route {route!r} uses invalid converter {raw_converter!r}." ) from e converters[parameter] = converter - parts.append("(?P<" + parameter + ">" + converter.regex + ")") + + start, end = match_.span() + parts.append(re.escape(route[previous_end:start])) + previous_end = end + parts.append(f"(?P<{parameter}>{converter.regex})") + + parts.append(re.escape(route[previous_end:])) if is_endpoint: parts.append(r"\Z") return "".join(parts), converters diff --git a/docs/releases/5.1.txt b/docs/releases/5.1.txt index 7fc794cd1d5b..4eab41394605 100644 --- a/docs/releases/5.1.txt +++ b/docs/releases/5.1.txt @@ -393,6 +393,9 @@ Miscellaneous :py:class:`html.parser.HTMLParser` subclasses. This results in a more robust and faster operation, but there may be small differences in the output. +* The undocumented ``django.urls.converters.get_converter()`` function is + removed. + .. _deprecated-features-5.1: Features deprecated in 5.1 diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py index 370e8695606c..78b71fe32577 100644 --- a/tests/urlpatterns/tests.py +++ b/tests/urlpatterns/tests.py @@ -246,14 +246,12 @@ class EmptyCBV(View): path("foo", EmptyCBV()) def test_whitespace_in_route(self): - msg = ( - "URL route 'space//extra/' cannot contain " - "whitespace in angle brackets <…>" - ) + msg = "URL route %r cannot contain whitespace in angle brackets <…>" for whitespace in string.whitespace: with self.subTest(repr(whitespace)): - with self.assertRaisesMessage(ImproperlyConfigured, msg % whitespace): - path("space//extra/" % whitespace, empty_view) + route = "space//extra/" % whitespace + with self.assertRaisesMessage(ImproperlyConfigured, msg % route): + path(route, empty_view) # Whitespaces are valid in paths. p = path("space%s//" % string.whitespace, empty_view) match = p.resolve("space%s/1/" % string.whitespace)