summaryrefslogtreecommitdiff
path: root/django/urls
diff options
context:
space:
mode:
Diffstat (limited to 'django/urls')
-rw-r--r--django/urls/__init__.py20
-rw-r--r--django/urls/conf.py31
-rw-r--r--django/urls/converters.py70
-rw-r--r--django/urls/resolvers.py415
4 files changed, 372 insertions, 164 deletions
diff --git a/django/urls/__init__.py b/django/urls/__init__.py
index f6d51e8a9f..e9e32ac5b9 100644
--- a/django/urls/__init__.py
+++ b/django/urls/__init__.py
@@ -3,19 +3,21 @@ from .base import (
is_valid_path, resolve, reverse, reverse_lazy, set_script_prefix,
set_urlconf, translate_url,
)
-from .conf import include
+from .conf import include, path, re_path
+from .converters import register_converter
from .exceptions import NoReverseMatch, Resolver404
from .resolvers import (
- LocaleRegexProvider, LocaleRegexURLResolver, RegexURLPattern,
- RegexURLResolver, ResolverMatch, get_ns_resolver, get_resolver,
+ LocalePrefixPattern, ResolverMatch, URLPattern, URLResolver,
+ get_ns_resolver, get_resolver,
)
from .utils import get_callable, get_mod_func
__all__ = [
- 'LocaleRegexProvider', 'LocaleRegexURLResolver', 'NoReverseMatch',
- 'RegexURLPattern', 'RegexURLResolver', 'Resolver404', 'ResolverMatch',
- 'clear_script_prefix', 'clear_url_caches', 'get_callable', 'get_mod_func',
- 'get_ns_resolver', 'get_resolver', 'get_script_prefix', 'get_urlconf',
- 'include', 'is_valid_path', 'resolve', 'reverse', 'reverse_lazy',
- 'set_script_prefix', 'set_urlconf', 'translate_url',
+ 'LocalePrefixPattern', 'NoReverseMatch', 'URLPattern',
+ 'URLResolver', 'Resolver404', 'ResolverMatch', 'clear_script_prefix',
+ 'clear_url_caches', 'get_callable', 'get_mod_func', 'get_ns_resolver',
+ 'get_resolver', 'get_script_prefix', 'get_urlconf', 'include',
+ 'is_valid_path', 'path', 're_path', 'register_converter', 'resolve',
+ 'reverse', 'reverse_lazy', 'set_script_prefix', 'set_urlconf',
+ 'translate_url',
]
diff --git a/django/urls/conf.py b/django/urls/conf.py
index 99bff55819..119e95df41 100644
--- a/django/urls/conf.py
+++ b/django/urls/conf.py
@@ -1,9 +1,12 @@
"""Functions for use in URLsconfs."""
+from functools import partial
from importlib import import_module
from django.core.exceptions import ImproperlyConfigured
-from .resolvers import LocaleRegexURLResolver
+from .resolvers import (
+ LocalePrefixPattern, RegexPattern, RoutePattern, URLPattern, URLResolver,
+)
def include(arg, namespace=None):
@@ -43,8 +46,32 @@ def include(arg, namespace=None):
# testcases will break).
if isinstance(patterns, (list, tuple)):
for url_pattern in patterns:
- if isinstance(url_pattern, LocaleRegexURLResolver):
+ pattern = getattr(url_pattern, 'pattern', None)
+ if isinstance(pattern, LocalePrefixPattern):
raise ImproperlyConfigured(
'Using i18n_patterns in an included URLconf is not allowed.'
)
return (urlconf_module, app_name, namespace)
+
+
+def _path(route, view, kwargs=None, name=None, Pattern=None):
+ if isinstance(view, (list, tuple)):
+ # For include(...) processing.
+ pattern = Pattern(route, is_endpoint=False)
+ urlconf_module, app_name, namespace = view
+ return URLResolver(
+ pattern,
+ urlconf_module,
+ kwargs,
+ app_name=app_name,
+ namespace=namespace,
+ )
+ elif callable(view):
+ pattern = Pattern(route, name=name, is_endpoint=True)
+ return URLPattern(pattern, view, kwargs, name)
+ else:
+ raise TypeError('view must be a callable or a list/tuple in the case of include().')
+
+
+path = partial(_path, Pattern=RoutePattern)
+re_path = partial(_path, Pattern=RegexPattern)
diff --git a/django/urls/converters.py b/django/urls/converters.py
new file mode 100644
index 0000000000..eb2a61971e
--- /dev/null
+++ b/django/urls/converters.py
@@ -0,0 +1,70 @@
+import uuid
+
+from django.utils import lru_cache
+
+
+class IntConverter:
+ regex = '[0-9]+'
+
+ def to_python(self, value):
+ return int(value)
+
+ def to_url(self, value):
+ return str(value)
+
+
+class StringConverter:
+ regex = '[^/]+'
+
+ def to_python(self, value):
+ return value
+
+ def to_url(self, value):
+ return value
+
+
+class UUIDConverter:
+ regex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
+
+ def to_python(self, value):
+ return uuid.UUID(value)
+
+ def to_url(self, value):
+ return str(value)
+
+
+class SlugConverter(StringConverter):
+ regex = '[-a-zA-Z0-9_]+'
+
+
+class PathConverter(StringConverter):
+ regex = '.+'
+
+
+DEFAULT_CONVERTERS = {
+ 'int': IntConverter(),
+ 'path': PathConverter(),
+ 'slug': SlugConverter(),
+ 'str': StringConverter(),
+ 'uuid': UUIDConverter(),
+}
+
+
+REGISTERED_CONVERTERS = {}
+
+
+def register_converter(converter, type_name):
+ REGISTERED_CONVERTERS[type_name] = converter()
+ get_converters.cache_clear()
+
+
+@lru_cache.lru_cache(maxsize=None)
+def get_converters():
+ converters = {}
+ converters.update(DEFAULT_CONVERTERS)
+ converters.update(REGISTERED_CONVERTERS)
+ return converters
+
+
+def get_converter(raw_converter):
+ return get_converters()[raw_converter]
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index ecad10acea..f91e821b38 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -21,6 +21,7 @@ from django.utils.http import RFC3986_SUBDELIMS
from django.utils.regex_helper import normalize
from django.utils.translation import get_language
+from .converters import get_converter
from .exceptions import NoReverseMatch, Resolver404
from .utils import get_callable
@@ -64,7 +65,7 @@ def get_resolver(urlconf=None):
if urlconf is None:
from django.conf import settings
urlconf = settings.ROOT_URLCONF
- return RegexURLResolver(r'^/', urlconf)
+ return URLResolver(RegexPattern(r'^/'), urlconf)
@functools.lru_cache(maxsize=None)
@@ -72,11 +73,14 @@ def get_ns_resolver(ns_pattern, resolver):
# Build a namespaced resolver for the given parent URLconf pattern.
# This makes it possible to have captured parameters in the parent
# URLconf pattern.
- ns_resolver = RegexURLResolver(ns_pattern, resolver.url_patterns)
- return RegexURLResolver(r'^/', [ns_resolver])
+ ns_resolver = URLResolver(RegexPattern(ns_pattern), resolver.url_patterns)
+ return URLResolver(RegexPattern(r'^/'), [ns_resolver])
class LocaleRegexDescriptor:
+ def __init__(self, attr):
+ self.attr = attr
+
def __get__(self, instance, cls=None):
"""
Return a compiled regular expression based on the active language.
@@ -86,46 +90,23 @@ class LocaleRegexDescriptor:
# As a performance optimization, if the given regex string is a regular
# string (not a lazily-translated string proxy), compile it once and
# avoid per-language compilation.
- if isinstance(instance._regex, str):
- instance.__dict__['regex'] = self._compile(instance._regex)
+ pattern = getattr(instance, self.attr)
+ if isinstance(pattern, str):
+ instance.__dict__['regex'] = instance._compile(pattern)
return instance.__dict__['regex']
language_code = get_language()
if language_code not in instance._regex_dict:
- instance._regex_dict[language_code] = self._compile(str(instance._regex))
+ instance._regex_dict[language_code] = instance._compile(str(pattern))
return instance._regex_dict[language_code]
- def _compile(self, regex):
- """
- Compile and return the given regular expression.
- """
- try:
- return re.compile(regex)
- except re.error as e:
- raise ImproperlyConfigured(
- '"%s" is not a valid regular expression: %s' % (regex, e)
- )
-
-
-class LocaleRegexProvider:
- """
- A mixin to provide a default regex property which can vary by active
- language.
- """
- def __init__(self, regex):
- # regex is either a string representing a regular expression, or a
- # translatable string (using gettext_lazy) representing a regular
- # expression.
- self._regex = regex
- self._regex_dict = {}
-
- regex = LocaleRegexDescriptor()
+class CheckURLMixin:
def describe(self):
"""
Format the URL pattern for display in warning messages.
"""
- description = "'{}'".format(self.regex.pattern)
- if getattr(self, 'name', False):
+ description = "'{}'".format(self)
+ if self.name:
description += " [name='{}']".format(self.name)
return description
@@ -138,9 +119,9 @@ class LocaleRegexProvider:
# Skip check as it can be useful to start a URL pattern with a slash
# when APPEND_SLASH=False.
return []
- if (regex_pattern.startswith('/') or regex_pattern.startswith('^/')) and not regex_pattern.endswith('/'):
+ if any(regex_pattern.startswith(x) for x in ('/', '^/', '^\/')) and not regex_pattern.endswith('/'):
warning = Warning(
- "Your URL pattern {} has a regex beginning with a '/'. Remove this "
+ "Your URL pattern {} has a route beginning with a '/'. Remove this "
"slash as it is unnecessary. If this pattern is targeted in an "
"include(), ensure the include() pattern has a trailing '/'.".format(
self.describe()
@@ -152,30 +133,195 @@ class LocaleRegexProvider:
return []
-class RegexURLPattern(LocaleRegexProvider):
- def __init__(self, regex, callback, default_args=None, name=None):
- LocaleRegexProvider.__init__(self, regex)
+class RegexPattern(CheckURLMixin):
+ regex = LocaleRegexDescriptor('_regex')
+
+ def __init__(self, regex, name=None, is_endpoint=False):
+ self._regex = regex
+ self._regex_dict = {}
+ self._is_endpoint = is_endpoint
+ self.name = name
+ self.converters = {}
+
+ def match(self, path):
+ match = self.regex.search(path)
+ if match:
+ # If there are any named groups, use those as kwargs, ignoring
+ # non-named groups. Otherwise, pass all non-named arguments as
+ # positional arguments.
+ kwargs = match.groupdict()
+ args = () if kwargs else match.groups()
+ return path[match.end():], args, kwargs
+ return None
+
+ def check(self):
+ warnings = []
+ warnings.extend(self._check_pattern_startswith_slash())
+ if not self._is_endpoint:
+ warnings.extend(self._check_include_trailing_dollar())
+ return warnings
+
+ def _check_include_trailing_dollar(self):
+ regex_pattern = self.regex.pattern
+ if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
+ return [Warning(
+ "Your URL pattern {} uses include with a route ending with a '$'. "
+ "Remove the dollar from the route to avoid problems including "
+ "URLs.".format(self.describe()),
+ id='urls.W001',
+ )]
+ else:
+ return []
+
+ def _compile(self, regex):
+ """Compile and return the given regular expression."""
+ try:
+ return re.compile(regex)
+ except re.error as e:
+ raise ImproperlyConfigured(
+ '"%s" is not a valid regular expression: %s' % (regex, e)
+ )
+
+ def __str__(self):
+ return self._regex
+
+
+_PATH_PARAMETER_COMPONENT_RE = re.compile(
+ '<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
+)
+
+
+def _route_to_regex(route, is_endpoint=False):
+ """
+ Convert a path pattern into a regular expression. Return the regular
+ expression and a dictionary mapping the capture names to the converters.
+ For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
+ and {'pk': <django.urls.converters.IntConverter>}.
+ """
+ original_route = route
+ parts = ['^']
+ converters = {}
+ while True:
+ match = _PATH_PARAMETER_COMPONENT_RE.search(route)
+ if not match:
+ parts.append(re.escape(route))
+ break
+ parts.append(re.escape(route[:match.start()]))
+ route = route[match.end():]
+ parameter = match.group('parameter')
+ if not parameter.isidentifier():
+ raise ImproperlyConfigured(
+ "URL route '%s' uses parameter name %r which isn't a valid "
+ "Python identifier." % (original_route, parameter)
+ )
+ raw_converter = match.group('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)
+ except KeyError as e:
+ raise ImproperlyConfigured(
+ "URL route '%s' uses invalid converter %s." % (original_route, e)
+ )
+ converters[parameter] = converter
+ parts.append('(?P<' + parameter + '>' + converter.regex + ')')
+ if is_endpoint:
+ parts.append('$')
+ return ''.join(parts), converters
+
+
+class RoutePattern(CheckURLMixin):
+ regex = LocaleRegexDescriptor('_route')
+
+ def __init__(self, route, name=None, is_endpoint=False):
+ self._route = route
+ self._regex_dict = {}
+ self._is_endpoint = is_endpoint
+ self.name = name
+ self.converters = _route_to_regex(str(route), is_endpoint)[1]
+
+ def match(self, path):
+ match = self.regex.search(path)
+ if match:
+ # RoutePattern doesn't allow non-named groups so args are ignored.
+ kwargs = match.groupdict()
+ for key, value in kwargs.items():
+ converter = self.converters[key]
+ try:
+ kwargs[key] = converter.to_python(value)
+ except ValueError:
+ return None
+ return path[match.end():], (), kwargs
+ return None
+
+ def check(self):
+ return self._check_pattern_startswith_slash()
+
+ def _compile(self, route):
+ return re.compile(_route_to_regex(route, self._is_endpoint)[0])
+
+ def __str__(self):
+ return self._route
+
+
+class LocalePrefixPattern:
+ def __init__(self, prefix_default_language=True):
+ self.prefix_default_language = prefix_default_language
+ self.converters = {}
+
+ @property
+ def regex(self):
+ # This is only used by reverse() and cached in _reverse_dict.
+ return re.compile(self.language_prefix)
+
+ @property
+ def language_prefix(self):
+ language_code = get_language() or settings.LANGUAGE_CODE
+ if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
+ return ''
+ else:
+ return '%s/' % language_code
+
+ def match(self, path):
+ language_prefix = self.language_prefix
+ if path.startswith(language_prefix):
+ return path[len(language_prefix):], (), {}
+ return None
+
+ def check(self):
+ return []
+
+ def describe(self):
+ return "'{}'".format(self)
+
+ def __str__(self):
+ return self.language_prefix
+
+
+class URLPattern:
+ def __init__(self, pattern, callback, default_args=None, name=None):
+ self.pattern = pattern
self.callback = callback # the view
self.default_args = default_args or {}
self.name = name
def __repr__(self):
- return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
+ return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
def check(self):
warnings = self._check_pattern_name()
- if not warnings:
- warnings = self._check_pattern_startswith_slash()
+ warnings.extend(self.pattern.check())
return warnings
def _check_pattern_name(self):
"""
Check that the pattern name does not contain a colon.
"""
- if self.name is not None and ":" in self.name:
+ if self.pattern.name is not None and ":" in self.pattern.name:
warning = Warning(
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
- "avoid ambiguous namespace references.".format(self.describe()),
+ "avoid ambiguous namespace references.".format(self.pattern.describe()),
id="urls.W003",
)
return [warning]
@@ -183,16 +329,12 @@ class RegexURLPattern(LocaleRegexProvider):
return []
def resolve(self, path):
- match = self.regex.search(path)
+ match = self.pattern.match(path)
if match:
- # If there are any named groups, use those as kwargs, ignoring
- # non-named groups. Otherwise, pass all non-named arguments as
- # positional arguments.
- kwargs = match.groupdict()
- args = () if kwargs else match.groups()
- # In both cases, pass any extra_kwargs as **kwargs.
+ new_path, args, kwargs = match
+ # Pass any extra_kwargs as **kwargs.
kwargs.update(self.default_args)
- return ResolverMatch(self.callback, args, kwargs, self.name)
+ return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
@cached_property
def lookup_str(self):
@@ -210,9 +352,9 @@ class RegexURLPattern(LocaleRegexProvider):
return callback.__module__ + "." + callback.__qualname__
-class RegexURLResolver(LocaleRegexProvider):
- def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
- LocaleRegexProvider.__init__(self, regex)
+class URLResolver:
+ def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
+ self.pattern = pattern
# urlconf_name is the dotted Python path to the module defining
# urlpatterns. It may also be an object with an urlpatterns attribute
# or urlpatterns itself.
@@ -238,33 +380,17 @@ class RegexURLResolver(LocaleRegexProvider):
urlconf_repr = repr(self.urlconf_name)
return '<%s %s (%s:%s) %s>' % (
self.__class__.__name__, urlconf_repr, self.app_name,
- self.namespace, self.regex.pattern,
+ self.namespace, self.pattern.describe(),
)
def check(self):
- warnings = self._check_include_trailing_dollar()
+ warnings = []
for pattern in self.url_patterns:
warnings.extend(check_resolver(pattern))
if not warnings:
- warnings = self._check_pattern_startswith_slash()
+ warnings = self.pattern.check()
return warnings
- def _check_include_trailing_dollar(self):
- """
- Check that include is not used with a regex ending with a dollar.
- """
- regex_pattern = self.regex.pattern
- if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
- warning = Warning(
- "Your URL pattern {} uses include with a regex ending with a '$'. "
- "Remove the dollar from the regex to avoid problems including "
- "URLs.".format(self.describe()),
- id="urls.W001",
- )
- return [warning]
- else:
- return []
-
def _populate(self):
# Short-circuit if called recursively in this thread to prevent
# infinite recursion. Concurrent threads may call this at the same
@@ -277,47 +403,52 @@ class RegexURLResolver(LocaleRegexProvider):
namespaces = {}
apps = {}
language_code = get_language()
- for pattern in reversed(self.url_patterns):
- if isinstance(pattern, RegexURLPattern):
- self._callback_strs.add(pattern.lookup_str)
- p_pattern = pattern.regex.pattern
- if p_pattern.startswith('^'):
- p_pattern = p_pattern[1:]
- if isinstance(pattern, RegexURLResolver):
- if pattern.namespace:
- namespaces[pattern.namespace] = (p_pattern, pattern)
- if pattern.app_name:
- apps.setdefault(pattern.app_name, []).append(pattern.namespace)
- else:
- parent_pat = pattern.regex.pattern
- for name in pattern.reverse_dict:
- for matches, pat, defaults in pattern.reverse_dict.getlist(name):
- new_matches = normalize(parent_pat + pat)
- lookups.appendlist(
- name,
- (
- new_matches,
- p_pattern + pat,
- dict(defaults, **pattern.default_kwargs),
+ try:
+ for url_pattern in reversed(self.url_patterns):
+ p_pattern = url_pattern.pattern.regex.pattern
+ 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,
+ (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
+ )
+ if url_pattern.name is not None:
+ lookups.appendlist(
+ url_pattern.name,
+ (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
+ )
+ else: # url_pattern is a URLResolver.
+ url_pattern._populate()
+ if url_pattern.app_name:
+ apps.setdefault(url_pattern.app_name, []).append(url_pattern.namespace)
+ namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
+ else:
+ for name in url_pattern.reverse_dict:
+ for matches, pat, defaults, converters in url_pattern.reverse_dict.getlist(name):
+ new_matches = normalize(p_pattern + pat)
+ lookups.appendlist(
+ name,
+ (
+ new_matches,
+ p_pattern + pat,
+ dict(defaults, **url_pattern.default_kwargs),
+ dict(self.pattern.converters, **converters)
+ )
)
- )
- for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
- namespaces[namespace] = (p_pattern + prefix, sub_pattern)
- for app_name, namespace_list in pattern.app_dict.items():
- apps.setdefault(app_name, []).extend(namespace_list)
- if not getattr(pattern._local, 'populating', False):
- pattern._populate()
- self._callback_strs.update(pattern._callback_strs)
- else:
- bits = normalize(p_pattern)
- lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
- if pattern.name is not None:
- lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
- self._reverse_dict[language_code] = lookups
- self._namespace_dict[language_code] = namespaces
- self._app_dict[language_code] = apps
- self._populated = True
- self._local.populating = False
+ for namespace, (prefix, sub_pattern) in url_pattern.namespace_dict.items():
+ 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
+ self._populated = True
+ finally:
+ self._local.populating = False
@property
def reverse_dict(self):
@@ -348,9 +479,9 @@ class RegexURLResolver(LocaleRegexProvider):
def resolve(self, path):
path = str(path) # path may be a reverse_lazy object
tried = []
- match = self.regex.search(path)
+ match = self.pattern.match(path)
if match:
- new_path = path[match.end():]
+ new_path, args, kwargs = match
for pattern in self.url_patterns:
try:
sub_match = pattern.resolve(new_path)
@@ -363,15 +494,14 @@ class RegexURLResolver(LocaleRegexProvider):
else:
if sub_match:
# Merge captured arguments in match with submatch
- sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
+ sub_match_dict = dict(kwargs, **self.default_kwargs)
+ # Update the sub_match_dict with the kwargs from the sub_match.
sub_match_dict.update(sub_match.kwargs)
-
# If there are *any* named groups, ignore all non-named groups.
# Otherwise, pass all non-named arguments as positional arguments.
sub_match_args = sub_match.args
if not sub_match_dict:
- sub_match_args = match.groups() + sub_match.args
-
+ sub_match_args = args + sub_match.args
return ResolverMatch(
sub_match.func,
sub_match_args,
@@ -421,20 +551,18 @@ class RegexURLResolver(LocaleRegexProvider):
def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
if args and kwargs:
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
- text_args = [str(v) for v in args]
- text_kwargs = {k: str(v) for (k, v) in kwargs.items()}
if not self._populated:
self._populate()
possibilities = self.reverse_dict.getlist(lookup_view)
- for possibility, pattern, defaults in possibilities:
+ for possibility, pattern, defaults, converters in possibilities:
for result, params in possibility:
if args:
if len(args) != len(params):
continue
- candidate_subs = dict(zip(params, text_args))
+ candidate_subs = dict(zip(params, args))
else:
if set(kwargs).symmetric_difference(params).difference(defaults):
continue
@@ -445,16 +573,23 @@ class RegexURLResolver(LocaleRegexProvider):
break
if not matches:
continue
- candidate_subs = text_kwargs
+ candidate_subs = kwargs
+ # Convert the candidate subs to text using Converter.to_url().
+ text_candidate_subs = {}
+ for k, v in candidate_subs.items():
+ if k in converters:
+ text_candidate_subs[k] = converters[k].to_url(v)
+ else:
+ text_candidate_subs[k] = str(v)
# WSGI provides decoded URLs, without %xx escapes, and the URL
# resolver operates on such URLs. First substitute arguments
# without quoting to build a decoded URL and look for a match.
# Then, if we have a match, redo the substitution with quoted
# arguments in order to return a properly encoded URL.
candidate_pat = _prefix.replace('%', '%%') + result
- if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs):
+ if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
# safe characters from `pchar` definition of RFC 3986
- url = quote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
+ url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
# Don't allow construction of scheme relative urls.
if url.startswith('//'):
url = '/%%2F%s' % url[2:]
@@ -468,7 +603,7 @@ class RegexURLResolver(LocaleRegexProvider):
else:
lookup_view_s = lookup_view
- patterns = [pattern for (possibility, pattern, defaults) in possibilities]
+ patterns = [pattern for (_, pattern, _, _) in possibilities]
if patterns:
if args:
arg_msg = "arguments '%s'" % (args,)
@@ -486,29 +621,3 @@ class RegexURLResolver(LocaleRegexProvider):
"a valid view function or pattern name." % {'view': lookup_view_s}
)
raise NoReverseMatch(msg)
-
-
-class LocaleRegexURLResolver(RegexURLResolver):
- """
- A URL resolver that always matches the active language code as URL prefix.
-
- Rather than taking a regex argument, we just override the ``regex``
- function to always return the active language-code as regex.
- """
- def __init__(
- self, urlconf_name, default_kwargs=None, app_name=None, namespace=None,
- prefix_default_language=True,
- ):
- super().__init__(None, urlconf_name, default_kwargs, app_name, namespace)
- self.prefix_default_language = prefix_default_language
-
- @property
- def regex(self):
- language_code = get_language() or settings.LANGUAGE_CODE
- if language_code not in self._regex_dict:
- if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
- regex_string = ''
- else:
- regex_string = '^%s/' % language_code
- self._regex_dict[language_code] = re.compile(regex_string)
- return self._regex_dict[language_code]