summaryrefslogtreecommitdiff
path: root/django/urls
diff options
context:
space:
mode:
authorSjoerd Job Postmus <sjoerdjob@sjec.nl>2016-10-20 19:29:04 +0200
committerTim Graham <timograham@gmail.com>2017-09-20 18:04:42 -0400
commitdf41b5a05d4e00e80e73afe629072e37873e767a (patch)
treebaaf71ae695e2d3af604ea0d663284cb406c71e4 /django/urls
parentc4c128d67c7dc2830631c6859a204c9d259f1fb1 (diff)
Fixed #28593 -- Added a simplified URL routing syntax per DEP 0201.
Thanks Aymeric Augustin for shepherding the DEP and patch review. Thanks Marten Kenbeek and Tim Graham for contributing to the code. Thanks Tom Christie, Shai Berger, and Tim Graham for the docs.
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]