"""
This module converts requested URLs to callback view functions.
URLResolver is the main class here. Its resolve() method takes a URL (as
a string) and returns a ResolverMatch object which provides access to all
attributes of the resolved URL match.
"""
import functools
import inspect
import re
import string
from importlib import import_module
from pickle import PicklingError
from urllib.parse import quote
from asgiref.local import Local
from django.conf import settings
from django.core.checks import Error, Warning
from django.core.checks.urls import check_resolver
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict
from django.utils.functional import cached_property
from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
from django.utils.regex_helper import _lazy_re_compile, normalize
from django.utils.translation import get_language
from .converters import get_converter
from .exceptions import NoReverseMatch, Resolver404
from .utils import get_callable
[docs]
class ResolverMatch:
def __init__(
self,
func,
args,
kwargs,
url_name=None,
app_names=None,
namespaces=None,
route=None,
tried=None,
captured_kwargs=None,
extra_kwargs=None,
):
self.func = func
self.args = args
self.kwargs = kwargs
self.url_name = url_name
self.route = route
self.tried = tried
self.captured_kwargs = captured_kwargs
self.extra_kwargs = extra_kwargs
# If a URLRegexResolver doesn't have a namespace or app_name, it passes
# in an empty value.
self.app_names = [x for x in app_names if x] if app_names else []
self.app_name = ":".join(self.app_names)
self.namespaces = [x for x in namespaces if x] if namespaces else []
self.namespace = ":".join(self.namespaces)
if hasattr(func, "view_class"):
func = func.view_class
if not hasattr(func, "__name__"):
# A class-based view
self._func_path = func.__class__.__module__ + "." + func.__class__.__name__
else:
# A function-based view
self._func_path = func.__module__ + "." + func.__name__
view_path = url_name or self._func_path
self.view_name = ":".join(self.namespaces + [view_path])
def __getitem__(self, index):
return (self.func, self.args, self.kwargs)[index]
def __repr__(self):
if isinstance(self.func, functools.partial):
func = repr(self.func)
else:
func = self._func_path
return (
"ResolverMatch(func=%s, args=%r, kwargs=%r, url_name=%r, "
"app_names=%r, namespaces=%r, route=%r%s%s)"
% (
func,
self.args,
self.kwargs,
self.url_name,
self.app_names,
self.namespaces,
self.route,
f", captured_kwargs={self.captured_kwargs!r}"
if self.captured_kwargs
else "",
f", extra_kwargs={self.extra_kwargs!r}" if self.extra_kwargs else "",
)
)
def __reduce_ex__(self, protocol):
raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
def get_resolver(urlconf=None):
if urlconf is None:
urlconf = settings.ROOT_URLCONF
return _get_cached_resolver(urlconf)
@functools.cache
def _get_cached_resolver(urlconf=None):
return URLResolver(RegexPattern(r"^/"), urlconf)
@functools.cache
def get_ns_resolver(ns_pattern, resolver, converters):
# Build a namespaced resolver for the given parent URLconf pattern.
# This makes it possible to have captured parameters in the parent
# URLconf pattern.
pattern = RegexPattern(ns_pattern)
pattern.converters = dict(converters)
ns_resolver = URLResolver(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.
"""
if instance is None:
return self
# 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.
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] = instance._compile(str(pattern))
return instance._regex_dict[language_code]
class CheckURLMixin:
def describe(self):
"""
Format the URL pattern for display in warning messages.
"""
description = "'{}'".format(self)
if self.name:
description += " [name='{}']".format(self.name)
return description
def _check_pattern_startswith_slash(self):
"""
Check that the pattern does not begin with a forward slash.
"""
regex_pattern = self.regex.pattern
if not settings.APPEND_SLASH:
# Skip check as it can be useful to start a URL pattern with a slash
# when APPEND_SLASH=False.
return []
if regex_pattern.startswith(("/", "^/", "^\\/")) and not regex_pattern.endswith(
"/"
):
warning = Warning(
"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()
),
id="urls.W002",
)
return [warning]
else:
return []
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.fullmatch(path)
if self._is_endpoint and self.regex.pattern.endswith("$")
else 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()
kwargs = {k: v for k, v in kwargs.items() if v is not None}
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)
) from e
def __str__(self):
return str(self._regex)
_PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>[^>]+)>"
)
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
elif not set(match.group()).isdisjoint(string.whitespace):
raise ImproperlyConfigured(
"URL route '%s' cannot contain whitespace in angle brackets "
"<…>." % original_route
)
parts.append(re.escape(route[: match.start()]))
route = route[match.end() :]
parameter = match["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["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 %r uses invalid converter %r."
% (original_route, raw_converter)
) from e
converters[parameter] = converter
parts.append("(?P<" + parameter + ">" + converter.regex + ")")
if is_endpoint:
parts.append(r"\Z")
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):
warnings = [
*self._check_pattern_startswith_slash(),
*self._check_pattern_unmatched_angle_brackets(),
]
route = self._route
if "(?P<" in route or route.startswith("^") or route.endswith("$"):
warnings.append(
Warning(
"Your URL pattern {} has a route that contains '(?P<', begins "
"with a '^', or ends with a '$'. This was likely an oversight "
"when migrating to django.urls.path().".format(self.describe()),
id="2_0.W001",
)
)
return warnings
def _check_pattern_unmatched_angle_brackets(self):
warnings = []
msg = "Your URL pattern %s has an unmatched '%s' bracket."
brackets = re.findall(r"[<>]", str(self._route))
open_bracket_counter = 0
for bracket in brackets:
if bracket == "<":
open_bracket_counter += 1
elif bracket == ">":
open_bracket_counter -= 1
if open_bracket_counter < 0:
warnings.append(
Warning(msg % (self.describe(), ">"), id="urls.W010")
)
open_bracket_counter = 0
if open_bracket_counter > 0:
warnings.append(Warning(msg % (self.describe(), "<"), id="urls.W010"))
return warnings
def _compile(self, route):
return re.compile(_route_to_regex(route, self._is_endpoint)[0])
def __str__(self):
return str(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(re.escape(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.removeprefix(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>" % (self.__class__.__name__, self.pattern.describe())
def check(self):
warnings = self._check_pattern_name()
warnings.extend(self.pattern.check())
warnings.extend(self._check_callback())
return warnings
def _check_pattern_name(self):
"""
Check that the pattern name does not contain a colon.
"""
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.pattern.describe()),
id="urls.W003",
)
return [warning]
else:
return []
def _check_callback(self):
from django.views import View
view = self.callback
if inspect.isclass(view) and issubclass(view, View):
return [
Error(
"Your URL pattern %s has an invalid view, pass %s.as_view() "
"instead of %s."
% (
self.pattern.describe(),
view.__name__,
view.__name__,
),
id="urls.E009",
)
]
return []
def resolve(self, path):
match = self.pattern.match(path)
if match:
new_path, args, captured_kwargs = match
# Pass any default args as **kwargs.
kwargs = {**captured_kwargs, **self.default_args}
return ResolverMatch(
self.callback,
args,
kwargs,
self.pattern.name,
route=str(self.pattern),
captured_kwargs=captured_kwargs,
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:
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.
self.urlconf_name = urlconf_name
self.callback = None
self.default_kwargs = default_kwargs or {}
self.namespace = namespace
self.app_name = app_name
self._reverse_dict = {}
self._namespace_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._local = Local()
def __repr__(self):
if isinstance(self.urlconf_name, list) and self.urlconf_name:
# Don't bother to output the whole list, it can be huge
urlconf_repr = "<%s list>" % self.urlconf_name[0].__class__.__name__
else:
urlconf_repr = repr(self.urlconf_name)
return "<%s %s (%s:%s) %s>" % (
self.__class__.__name__,
urlconf_repr,
self.app_name,
self.namespace,
self.pattern.describe(),
)
def check(self):
messages = []
for pattern in self.url_patterns:
messages.extend(check_resolver(pattern))
messages.extend(self._check_custom_error_handlers())
return messages or self.pattern.check()
def _check_custom_error_handlers(self):
messages = []
# All handlers take (request, exception) arguments except handler500
# which takes (request).
for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]:
try:
handler = self.resolve_error_handler(status_code)
except (ImportError, ViewDoesNotExist) as e:
path = getattr(self.urlconf_module, "handler%s" % status_code)
msg = (
"The custom handler{status_code} view '{path}' could not be "
"imported."
).format(status_code=status_code, path=path)
messages.append(Error(msg, hint=str(e), id="urls.E008"))
continue
signature = inspect.signature(handler)
args = [None] * num_parameters
try:
signature.bind(*args)
except TypeError:
msg = (
"The custom handler{status_code} view '{path}' does not "
"take the correct number of arguments ({args})."
).format(
status_code=status_code,
path=handler.__module__ + "." + handler.__qualname__,
args="request, exception" if num_parameters == 2 else "request",
)
messages.append(Error(msg, id="urls.E007"))
return messages
def _populate(self):
# Short-circuit if called recursively in this thread to prevent
# infinite recursion. Concurrent threads may call this at the same
# time and will need to continue, so set 'populating' on a
# thread-local variable.
if getattr(self._local, "populating", False):
return
try:
self._local.populating = True
lookups = MultiValueDict()
namespaces = {}
apps = {}
language_code = get_language()
for url_pattern in reversed(self.url_patterns):
p_pattern = url_pattern.pattern.regex.pattern
p_pattern = p_pattern.removeprefix("^")
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,
{**defaults, **url_pattern.default_kwargs},
{
**self.pattern.converters,
**url_pattern.pattern.converters,
**converters,
},
),
)
for namespace, (
prefix,
sub_pattern,
) in url_pattern.namespace_dict.items():
current_converters = url_pattern.pattern.converters
sub_pattern.pattern.converters.update(current_converters)
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):
language_code = get_language()
if language_code not in self._reverse_dict:
self._populate()
return self._reverse_dict[language_code]
@property
def namespace_dict(self):
language_code = get_language()
if language_code not in self._namespace_dict:
self._populate()
return self._namespace_dict[language_code]
@property
def app_dict(self):
language_code = get_language()
if language_code not in self._app_dict:
self._populate()
return self._app_dict[language_code]
@staticmethod
def _extend_tried(tried, pattern, sub_tried=None):
if sub_tried is None:
tried.append([pattern])
else:
tried.extend([pattern, *t] for t in sub_tried)
@staticmethod
def _join_route(route1, route2):
"""Join two routes, without the starting ^ in the second route."""
if not route1:
return route2
route2 = route2.removeprefix("^")
return route1 + route2
def _is_callback(self, name):
if not self._populated:
self._populate()
return name in self._callback_strs
def resolve(self, path):
path = str(path) # path may be a reverse_lazy object
tried = []
match = self.pattern.match(path)
if match:
new_path, args, kwargs = match
for pattern in self.url_patterns:
try:
sub_match = pattern.resolve(new_path)
except Resolver404 as e:
self._extend_tried(tried, pattern, e.args[0].get("tried"))
else:
if sub_match:
# Merge captured arguments in match with submatch
sub_match_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 = args + sub_match.args
current_route = (
""
if isinstance(pattern, URLPattern)
else str(pattern.pattern)
)
self._extend_tried(tried, pattern, sub_match.tried)
return ResolverMatch(
sub_match.func,
sub_match_args,
sub_match_dict,
sub_match.url_name,
[self.app_name] + sub_match.app_names,
[self.namespace] + sub_match.namespaces,
self._join_route(current_route, sub_match.route),
tried,
captured_kwargs=sub_match.captured_kwargs,
extra_kwargs={
**self.default_kwargs,
**sub_match.extra_kwargs,
},
)
tried.append([pattern])
raise Resolver404({"tried": tried, "path": new_path})
raise Resolver404({"path": path})
@cached_property
def urlconf_module(self):
if isinstance(self.urlconf_name, str):
return import_module(self.urlconf_name)
else:
return self.urlconf_name
@cached_property
def url_patterns(self):
# urlconf_module might be a valid set of patterns, so we default to it
patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
try:
iter(patterns)
except TypeError as e:
msg = (
"The included URLconf '{name}' does not appear to have "
"any patterns in it. If you see the 'urlpatterns' variable "
"with valid patterns in the file then the issue is probably "
"caused by a circular import."
)
raise ImproperlyConfigured(msg.format(name=self.urlconf_name)) from e
return patterns
def resolve_error_handler(self, view_type):
callback = getattr(self.urlconf_module, "handler%s" % view_type, None)
if not callback:
# No handler specified in file; use lazy import, since
# django.conf.urls imports this file.
from django.conf import urls
callback = getattr(urls, "handler%s" % view_type)
return get_callable(callback)
def reverse(self, lookup_view, *args, **kwargs):
return self._reverse_with_prefix(lookup_view, "", *args, **kwargs)
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()!")
if not self._populated:
self._populate()
possibilities = self.reverse_dict.getlist(lookup_view)
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, args))
else:
if set(kwargs).symmetric_difference(params).difference(defaults):
continue
matches = True
for k, v in defaults.items():
if k in params:
continue
if kwargs.get(k, v) != v:
matches = False
break
if not matches:
continue
candidate_subs = kwargs
# Convert the candidate subs to text using Converter.to_url().
text_candidate_subs = {}
match = True
for k, v in candidate_subs.items():
if k in converters:
try:
text_candidate_subs[k] = converters[k].to_url(v)
except ValueError:
match = False
break
else:
text_candidate_subs[k] = str(v)
if not match:
continue
# 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 % text_candidate_subs,
):
# safe characters from `pchar` definition of RFC 3986
url = quote(
candidate_pat % text_candidate_subs,
safe=RFC3986_SUBDELIMS + "/~:@",
)
# Don't allow construction of scheme relative urls.
return escape_leading_slashes(url)
# lookup_view can be URL name or callable, but callables are not
# friendly in error messages.
m = getattr(lookup_view, "__module__", None)
n = getattr(lookup_view, "__name__", None)
if m is not None and n is not None:
lookup_view_s = "%s.%s" % (m, n)
else:
lookup_view_s = lookup_view
patterns = [pattern for (_, pattern, _, _) in possibilities]
if patterns:
if args:
arg_msg = "arguments '%s'" % (args,)
elif kwargs:
arg_msg = "keyword arguments '%s'" % kwargs
else:
arg_msg = "no arguments"
msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
lookup_view_s,
arg_msg,
len(patterns),
patterns,
)
else:
msg = (
"Reverse for '%(view)s' not found. '%(view)s' is not "
"a valid view function or pattern name." % {"view": lookup_view_s}
)
raise NoReverseMatch(msg)
Jan 15, 2024