""" 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 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.lru_cache(maxsize=None) def _get_cached_resolver(urlconf=None): return URLResolver(RegexPattern(r"^/"), urlconf) @functools.lru_cache(maxsize=None) 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[^>:]+):)?(?P[^>]+)>" ) 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/' returns '^foo\\/(?P[0-9]+)' and {'pk': }. """ 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() 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 _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(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>" % (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, ) 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 = {} 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 if p_pattern.startswith("^"): p_pattern = p_pattern[1:] if isinstance(url_pattern, URLPattern): 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._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 if route2.startswith("^"): route2 = route2[1:] return route1 + route2 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)