Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/urls/resolvers.py: 21%
463 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1"""
2This module converts requested URLs to callback view functions.
4URLResolver is the main class here. Its resolve() method takes a URL (as
5a string) and returns a ResolverMatch object which provides access to all
6attributes of the resolved URL match.
7"""
8import functools
9import inspect
10import re
11import string
12from importlib import import_module
13from pickle import PicklingError
14from urllib.parse import quote
16from asgiref.local import Local
18from django.conf import settings
19from django.core.checks import Error, Warning
20from django.core.checks.urls import check_resolver
21from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
22from django.utils.datastructures import MultiValueDict
23from django.utils.functional import cached_property
24from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
25from django.utils.regex_helper import _lazy_re_compile, normalize
26from django.utils.translation import get_language
28from .converters import get_converter
29from .exceptions import NoReverseMatch, Resolver404
30from .utils import get_callable
33class ResolverMatch:
34 def __init__(
35 self,
36 func,
37 args,
38 kwargs,
39 url_name=None,
40 app_names=None,
41 namespaces=None,
42 route=None,
43 tried=None,
44 captured_kwargs=None,
45 extra_kwargs=None,
46 ):
47 self.func = func
48 self.args = args
49 self.kwargs = kwargs
50 self.url_name = url_name
51 self.route = route
52 self.tried = tried
53 self.captured_kwargs = captured_kwargs
54 self.extra_kwargs = extra_kwargs
56 # If a URLRegexResolver doesn't have a namespace or app_name, it passes
57 # in an empty value.
58 self.app_names = [x for x in app_names if x] if app_names else []
59 self.app_name = ":".join(self.app_names)
60 self.namespaces = [x for x in namespaces if x] if namespaces else []
61 self.namespace = ":".join(self.namespaces)
63 if hasattr(func, "view_class"):
64 func = func.view_class
65 if not hasattr(func, "__name__"):
66 # A class-based view
67 self._func_path = func.__class__.__module__ + "." + func.__class__.__name__
68 else:
69 # A function-based view
70 self._func_path = func.__module__ + "." + func.__name__
72 view_path = url_name or self._func_path
73 self.view_name = ":".join(self.namespaces + [view_path])
75 def __getitem__(self, index):
76 return (self.func, self.args, self.kwargs)[index]
78 def __repr__(self):
79 if isinstance(self.func, functools.partial):
80 func = repr(self.func)
81 else:
82 func = self._func_path
83 return (
84 "ResolverMatch(func=%s, args=%r, kwargs=%r, url_name=%r, "
85 "app_names=%r, namespaces=%r, route=%r%s%s)"
86 % (
87 func,
88 self.args,
89 self.kwargs,
90 self.url_name,
91 self.app_names,
92 self.namespaces,
93 self.route,
94 f", captured_kwargs={self.captured_kwargs!r}"
95 if self.captured_kwargs
96 else "",
97 f", extra_kwargs={self.extra_kwargs!r}" if self.extra_kwargs else "",
98 )
99 )
101 def __reduce_ex__(self, protocol):
102 raise PicklingError(f"Cannot pickle {self.__class__.__qualname__}.")
105def get_resolver(urlconf=None):
106 if urlconf is None:
107 urlconf = settings.ROOT_URLCONF
108 return _get_cached_resolver(urlconf)
111@functools.lru_cache(maxsize=None)
112def _get_cached_resolver(urlconf=None):
113 return URLResolver(RegexPattern(r"^/"), urlconf)
116@functools.lru_cache(maxsize=None)
117def get_ns_resolver(ns_pattern, resolver, converters):
118 # Build a namespaced resolver for the given parent URLconf pattern.
119 # This makes it possible to have captured parameters in the parent
120 # URLconf pattern.
121 pattern = RegexPattern(ns_pattern)
122 pattern.converters = dict(converters)
123 ns_resolver = URLResolver(pattern, resolver.url_patterns)
124 return URLResolver(RegexPattern(r"^/"), [ns_resolver])
127class LocaleRegexDescriptor:
128 def __init__(self, attr):
129 self.attr = attr
131 def __get__(self, instance, cls=None):
132 """
133 Return a compiled regular expression based on the active language.
134 """
135 if instance is None:
136 return self
137 # As a performance optimization, if the given regex string is a regular
138 # string (not a lazily-translated string proxy), compile it once and
139 # avoid per-language compilation.
140 pattern = getattr(instance, self.attr)
141 if isinstance(pattern, str):
142 instance.__dict__["regex"] = instance._compile(pattern)
143 return instance.__dict__["regex"]
144 language_code = get_language()
145 if language_code not in instance._regex_dict:
146 instance._regex_dict[language_code] = instance._compile(str(pattern))
147 return instance._regex_dict[language_code]
150class CheckURLMixin:
151 def describe(self):
152 """
153 Format the URL pattern for display in warning messages.
154 """
155 description = "'{}'".format(self)
156 if self.name:
157 description += " [name='{}']".format(self.name)
158 return description
160 def _check_pattern_startswith_slash(self):
161 """
162 Check that the pattern does not begin with a forward slash.
163 """
164 regex_pattern = self.regex.pattern
165 if not settings.APPEND_SLASH:
166 # Skip check as it can be useful to start a URL pattern with a slash
167 # when APPEND_SLASH=False.
168 return []
169 if regex_pattern.startswith(("/", "^/", "^\\/")) and not regex_pattern.endswith(
170 "/"
171 ):
172 warning = Warning(
173 "Your URL pattern {} has a route beginning with a '/'. Remove this "
174 "slash as it is unnecessary. If this pattern is targeted in an "
175 "include(), ensure the include() pattern has a trailing '/'.".format(
176 self.describe()
177 ),
178 id="urls.W002",
179 )
180 return [warning]
181 else:
182 return []
185class RegexPattern(CheckURLMixin):
186 regex = LocaleRegexDescriptor("_regex")
188 def __init__(self, regex, name=None, is_endpoint=False):
189 self._regex = regex
190 self._regex_dict = {}
191 self._is_endpoint = is_endpoint
192 self.name = name
193 self.converters = {}
195 def match(self, path):
196 match = (
197 self.regex.fullmatch(path)
198 if self._is_endpoint and self.regex.pattern.endswith("$")
199 else self.regex.search(path)
200 )
201 if match:
202 # If there are any named groups, use those as kwargs, ignoring
203 # non-named groups. Otherwise, pass all non-named arguments as
204 # positional arguments.
205 kwargs = match.groupdict()
206 args = () if kwargs else match.groups()
207 kwargs = {k: v for k, v in kwargs.items() if v is not None}
208 return path[match.end() :], args, kwargs
209 return None
211 def check(self):
212 warnings = []
213 warnings.extend(self._check_pattern_startswith_slash())
214 if not self._is_endpoint:
215 warnings.extend(self._check_include_trailing_dollar())
216 return warnings
218 def _check_include_trailing_dollar(self):
219 regex_pattern = self.regex.pattern
220 if regex_pattern.endswith("$") and not regex_pattern.endswith(r"\$"):
221 return [
222 Warning(
223 "Your URL pattern {} uses include with a route ending with a '$'. "
224 "Remove the dollar from the route to avoid problems including "
225 "URLs.".format(self.describe()),
226 id="urls.W001",
227 )
228 ]
229 else:
230 return []
232 def _compile(self, regex):
233 """Compile and return the given regular expression."""
234 try:
235 return re.compile(regex)
236 except re.error as e:
237 raise ImproperlyConfigured(
238 '"%s" is not a valid regular expression: %s' % (regex, e)
239 ) from e
241 def __str__(self):
242 return str(self._regex)
245_PATH_PARAMETER_COMPONENT_RE = _lazy_re_compile(
246 r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>[^>]+)>"
247)
250def _route_to_regex(route, is_endpoint=False):
251 """
252 Convert a path pattern into a regular expression. Return the regular
253 expression and a dictionary mapping the capture names to the converters.
254 For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
255 and {'pk': <django.urls.converters.IntConverter>}.
256 """
257 original_route = route
258 parts = ["^"]
259 converters = {}
260 while True:
261 match = _PATH_PARAMETER_COMPONENT_RE.search(route)
262 if not match:
263 parts.append(re.escape(route))
264 break
265 elif not set(match.group()).isdisjoint(string.whitespace):
266 raise ImproperlyConfigured(
267 "URL route '%s' cannot contain whitespace in angle brackets "
268 "<…>." % original_route
269 )
270 parts.append(re.escape(route[: match.start()]))
271 route = route[match.end() :]
272 parameter = match["parameter"]
273 if not parameter.isidentifier():
274 raise ImproperlyConfigured(
275 "URL route '%s' uses parameter name %r which isn't a valid "
276 "Python identifier." % (original_route, parameter)
277 )
278 raw_converter = match["converter"]
279 if raw_converter is None:
280 # If a converter isn't specified, the default is `str`.
281 raw_converter = "str"
282 try:
283 converter = get_converter(raw_converter)
284 except KeyError as e:
285 raise ImproperlyConfigured(
286 "URL route %r uses invalid converter %r."
287 % (original_route, raw_converter)
288 ) from e
289 converters[parameter] = converter
290 parts.append("(?P<" + parameter + ">" + converter.regex + ")")
291 if is_endpoint:
292 parts.append(r"\Z")
293 return "".join(parts), converters
296class RoutePattern(CheckURLMixin):
297 regex = LocaleRegexDescriptor("_route")
299 def __init__(self, route, name=None, is_endpoint=False):
300 self._route = route
301 self._regex_dict = {}
302 self._is_endpoint = is_endpoint
303 self.name = name
304 self.converters = _route_to_regex(str(route), is_endpoint)[1]
306 def match(self, path):
307 match = self.regex.search(path)
308 if match:
309 # RoutePattern doesn't allow non-named groups so args are ignored.
310 kwargs = match.groupdict()
311 for key, value in kwargs.items():
312 converter = self.converters[key]
313 try:
314 kwargs[key] = converter.to_python(value)
315 except ValueError:
316 return None
317 return path[match.end() :], (), kwargs
318 return None
320 def check(self):
321 warnings = self._check_pattern_startswith_slash()
322 route = self._route
323 if "(?P<" in route or route.startswith("^") or route.endswith("$"):
324 warnings.append(
325 Warning(
326 "Your URL pattern {} has a route that contains '(?P<', begins "
327 "with a '^', or ends with a '$'. This was likely an oversight "
328 "when migrating to django.urls.path().".format(self.describe()),
329 id="2_0.W001",
330 )
331 )
332 return warnings
334 def _compile(self, route):
335 return re.compile(_route_to_regex(route, self._is_endpoint)[0])
337 def __str__(self):
338 return str(self._route)
341class LocalePrefixPattern:
342 def __init__(self, prefix_default_language=True):
343 self.prefix_default_language = prefix_default_language
344 self.converters = {}
346 @property
347 def regex(self):
348 # This is only used by reverse() and cached in _reverse_dict.
349 return re.compile(re.escape(self.language_prefix))
351 @property
352 def language_prefix(self):
353 language_code = get_language() or settings.LANGUAGE_CODE
354 if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
355 return ""
356 else:
357 return "%s/" % language_code
359 def match(self, path):
360 language_prefix = self.language_prefix
361 if path.startswith(language_prefix):
362 return path[len(language_prefix) :], (), {}
363 return None
365 def check(self):
366 return []
368 def describe(self):
369 return "'{}'".format(self)
371 def __str__(self):
372 return self.language_prefix
375class URLPattern:
376 def __init__(self, pattern, callback, default_args=None, name=None):
377 self.pattern = pattern
378 self.callback = callback # the view
379 self.default_args = default_args or {}
380 self.name = name
382 def __repr__(self):
383 return "<%s %s>" % (self.__class__.__name__, self.pattern.describe())
385 def check(self):
386 warnings = self._check_pattern_name()
387 warnings.extend(self.pattern.check())
388 warnings.extend(self._check_callback())
389 return warnings
391 def _check_pattern_name(self):
392 """
393 Check that the pattern name does not contain a colon.
394 """
395 if self.pattern.name is not None and ":" in self.pattern.name:
396 warning = Warning(
397 "Your URL pattern {} has a name including a ':'. Remove the colon, to "
398 "avoid ambiguous namespace references.".format(self.pattern.describe()),
399 id="urls.W003",
400 )
401 return [warning]
402 else:
403 return []
405 def _check_callback(self):
406 from django.views import View
408 view = self.callback
409 if inspect.isclass(view) and issubclass(view, View):
410 return [
411 Error(
412 "Your URL pattern %s has an invalid view, pass %s.as_view() "
413 "instead of %s."
414 % (
415 self.pattern.describe(),
416 view.__name__,
417 view.__name__,
418 ),
419 id="urls.E009",
420 )
421 ]
422 return []
424 def resolve(self, path):
425 match = self.pattern.match(path)
426 if match:
427 new_path, args, captured_kwargs = match
428 # Pass any default args as **kwargs.
429 kwargs = {**captured_kwargs, **self.default_args}
430 return ResolverMatch(
431 self.callback,
432 args,
433 kwargs,
434 self.pattern.name,
435 route=str(self.pattern),
436 captured_kwargs=captured_kwargs,
437 extra_kwargs=self.default_args,
438 )
440 @cached_property
441 def lookup_str(self):
442 """
443 A string that identifies the view (e.g. 'path.to.view_function' or
444 'path.to.ClassBasedView').
445 """
446 callback = self.callback
447 if isinstance(callback, functools.partial):
448 callback = callback.func
449 if hasattr(callback, "view_class"):
450 callback = callback.view_class
451 elif not hasattr(callback, "__name__"):
452 return callback.__module__ + "." + callback.__class__.__name__
453 return callback.__module__ + "." + callback.__qualname__
456class URLResolver:
457 def __init__(
458 self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None
459 ):
460 self.pattern = pattern
461 # urlconf_name is the dotted Python path to the module defining
462 # urlpatterns. It may also be an object with an urlpatterns attribute
463 # or urlpatterns itself.
464 self.urlconf_name = urlconf_name
465 self.callback = None
466 self.default_kwargs = default_kwargs or {}
467 self.namespace = namespace
468 self.app_name = app_name
469 self._reverse_dict = {}
470 self._namespace_dict = {}
471 self._app_dict = {}
472 # set of dotted paths to all functions and classes that are used in
473 # urlpatterns
474 self._callback_strs = set()
475 self._populated = False
476 self._local = Local()
478 def __repr__(self):
479 if isinstance(self.urlconf_name, list) and self.urlconf_name:
480 # Don't bother to output the whole list, it can be huge
481 urlconf_repr = "<%s list>" % self.urlconf_name[0].__class__.__name__
482 else:
483 urlconf_repr = repr(self.urlconf_name)
484 return "<%s %s (%s:%s) %s>" % (
485 self.__class__.__name__,
486 urlconf_repr,
487 self.app_name,
488 self.namespace,
489 self.pattern.describe(),
490 )
492 def check(self):
493 messages = []
494 for pattern in self.url_patterns:
495 messages.extend(check_resolver(pattern))
496 messages.extend(self._check_custom_error_handlers())
497 return messages or self.pattern.check()
499 def _check_custom_error_handlers(self):
500 messages = []
501 # All handlers take (request, exception) arguments except handler500
502 # which takes (request).
503 for status_code, num_parameters in [(400, 2), (403, 2), (404, 2), (500, 1)]:
504 try:
505 handler = self.resolve_error_handler(status_code)
506 except (ImportError, ViewDoesNotExist) as e:
507 path = getattr(self.urlconf_module, "handler%s" % status_code)
508 msg = (
509 "The custom handler{status_code} view '{path}' could not be "
510 "imported."
511 ).format(status_code=status_code, path=path)
512 messages.append(Error(msg, hint=str(e), id="urls.E008"))
513 continue
514 signature = inspect.signature(handler)
515 args = [None] * num_parameters
516 try:
517 signature.bind(*args)
518 except TypeError:
519 msg = (
520 "The custom handler{status_code} view '{path}' does not "
521 "take the correct number of arguments ({args})."
522 ).format(
523 status_code=status_code,
524 path=handler.__module__ + "." + handler.__qualname__,
525 args="request, exception" if num_parameters == 2 else "request",
526 )
527 messages.append(Error(msg, id="urls.E007"))
528 return messages
530 def _populate(self):
531 # Short-circuit if called recursively in this thread to prevent
532 # infinite recursion. Concurrent threads may call this at the same
533 # time and will need to continue, so set 'populating' on a
534 # thread-local variable.
535 if getattr(self._local, "populating", False):
536 return
537 try:
538 self._local.populating = True
539 lookups = MultiValueDict()
540 namespaces = {}
541 apps = {}
542 language_code = get_language()
543 for url_pattern in reversed(self.url_patterns):
544 p_pattern = url_pattern.pattern.regex.pattern
545 if p_pattern.startswith("^"):
546 p_pattern = p_pattern[1:]
547 if isinstance(url_pattern, URLPattern):
548 self._callback_strs.add(url_pattern.lookup_str)
549 bits = normalize(url_pattern.pattern.regex.pattern)
550 lookups.appendlist(
551 url_pattern.callback,
552 (
553 bits,
554 p_pattern,
555 url_pattern.default_args,
556 url_pattern.pattern.converters,
557 ),
558 )
559 if url_pattern.name is not None:
560 lookups.appendlist(
561 url_pattern.name,
562 (
563 bits,
564 p_pattern,
565 url_pattern.default_args,
566 url_pattern.pattern.converters,
567 ),
568 )
569 else: # url_pattern is a URLResolver.
570 url_pattern._populate()
571 if url_pattern.app_name:
572 apps.setdefault(url_pattern.app_name, []).append(
573 url_pattern.namespace
574 )
575 namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
576 else:
577 for name in url_pattern.reverse_dict:
578 for (
579 matches,
580 pat,
581 defaults,
582 converters,
583 ) in url_pattern.reverse_dict.getlist(name):
584 new_matches = normalize(p_pattern + pat)
585 lookups.appendlist(
586 name,
587 (
588 new_matches,
589 p_pattern + pat,
590 {**defaults, **url_pattern.default_kwargs},
591 {
592 **self.pattern.converters,
593 **url_pattern.pattern.converters,
594 **converters,
595 },
596 ),
597 )
598 for namespace, (
599 prefix,
600 sub_pattern,
601 ) in url_pattern.namespace_dict.items():
602 current_converters = url_pattern.pattern.converters
603 sub_pattern.pattern.converters.update(current_converters)
604 namespaces[namespace] = (p_pattern + prefix, sub_pattern)
605 for app_name, namespace_list in url_pattern.app_dict.items():
606 apps.setdefault(app_name, []).extend(namespace_list)
607 self._callback_strs.update(url_pattern._callback_strs)
608 self._namespace_dict[language_code] = namespaces
609 self._app_dict[language_code] = apps
610 self._reverse_dict[language_code] = lookups
611 self._populated = True
612 finally:
613 self._local.populating = False
615 @property
616 def reverse_dict(self):
617 language_code = get_language()
618 if language_code not in self._reverse_dict:
619 self._populate()
620 return self._reverse_dict[language_code]
622 @property
623 def namespace_dict(self):
624 language_code = get_language()
625 if language_code not in self._namespace_dict:
626 self._populate()
627 return self._namespace_dict[language_code]
629 @property
630 def app_dict(self):
631 language_code = get_language()
632 if language_code not in self._app_dict:
633 self._populate()
634 return self._app_dict[language_code]
636 @staticmethod
637 def _extend_tried(tried, pattern, sub_tried=None):
638 if sub_tried is None:
639 tried.append([pattern])
640 else:
641 tried.extend([pattern, *t] for t in sub_tried)
643 @staticmethod
644 def _join_route(route1, route2):
645 """Join two routes, without the starting ^ in the second route."""
646 if not route1:
647 return route2
648 if route2.startswith("^"):
649 route2 = route2[1:]
650 return route1 + route2
652 def _is_callback(self, name):
653 if not self._populated:
654 self._populate()
655 return name in self._callback_strs
657 def resolve(self, path):
658 path = str(path) # path may be a reverse_lazy object
659 tried = []
660 match = self.pattern.match(path)
661 if match:
662 new_path, args, kwargs = match
663 for pattern in self.url_patterns:
664 try:
665 sub_match = pattern.resolve(new_path)
666 except Resolver404 as e:
667 self._extend_tried(tried, pattern, e.args[0].get("tried"))
668 else:
669 if sub_match:
670 # Merge captured arguments in match with submatch
671 sub_match_dict = {**kwargs, **self.default_kwargs}
672 # Update the sub_match_dict with the kwargs from the sub_match.
673 sub_match_dict.update(sub_match.kwargs)
674 # If there are *any* named groups, ignore all non-named groups.
675 # Otherwise, pass all non-named arguments as positional
676 # arguments.
677 sub_match_args = sub_match.args
678 if not sub_match_dict:
679 sub_match_args = args + sub_match.args
680 current_route = (
681 ""
682 if isinstance(pattern, URLPattern)
683 else str(pattern.pattern)
684 )
685 self._extend_tried(tried, pattern, sub_match.tried)
686 return ResolverMatch(
687 sub_match.func,
688 sub_match_args,
689 sub_match_dict,
690 sub_match.url_name,
691 [self.app_name] + sub_match.app_names,
692 [self.namespace] + sub_match.namespaces,
693 self._join_route(current_route, sub_match.route),
694 tried,
695 captured_kwargs=sub_match.captured_kwargs,
696 extra_kwargs={
697 **self.default_kwargs,
698 **sub_match.extra_kwargs,
699 },
700 )
701 tried.append([pattern])
702 raise Resolver404({"tried": tried, "path": new_path})
703 raise Resolver404({"path": path})
705 @cached_property
706 def urlconf_module(self):
707 if isinstance(self.urlconf_name, str):
708 return import_module(self.urlconf_name)
709 else:
710 return self.urlconf_name
712 @cached_property
713 def url_patterns(self):
714 # urlconf_module might be a valid set of patterns, so we default to it
715 patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
716 try:
717 iter(patterns)
718 except TypeError as e:
719 msg = (
720 "The included URLconf '{name}' does not appear to have "
721 "any patterns in it. If you see the 'urlpatterns' variable "
722 "with valid patterns in the file then the issue is probably "
723 "caused by a circular import."
724 )
725 raise ImproperlyConfigured(msg.format(name=self.urlconf_name)) from e
726 return patterns
728 def resolve_error_handler(self, view_type):
729 callback = getattr(self.urlconf_module, "handler%s" % view_type, None)
730 if not callback:
731 # No handler specified in file; use lazy import, since
732 # django.conf.urls imports this file.
733 from django.conf import urls
735 callback = getattr(urls, "handler%s" % view_type)
736 return get_callable(callback)
738 def reverse(self, lookup_view, *args, **kwargs):
739 return self._reverse_with_prefix(lookup_view, "", *args, **kwargs)
741 def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
742 if args and kwargs:
743 raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
745 if not self._populated:
746 self._populate()
748 possibilities = self.reverse_dict.getlist(lookup_view)
750 for possibility, pattern, defaults, converters in possibilities:
751 for result, params in possibility:
752 if args:
753 if len(args) != len(params):
754 continue
755 candidate_subs = dict(zip(params, args))
756 else:
757 if set(kwargs).symmetric_difference(params).difference(defaults):
758 continue
759 matches = True
760 for k, v in defaults.items():
761 if k in params:
762 continue
763 if kwargs.get(k, v) != v:
764 matches = False
765 break
766 if not matches:
767 continue
768 candidate_subs = kwargs
769 # Convert the candidate subs to text using Converter.to_url().
770 text_candidate_subs = {}
771 match = True
772 for k, v in candidate_subs.items():
773 if k in converters:
774 try:
775 text_candidate_subs[k] = converters[k].to_url(v)
776 except ValueError:
777 match = False
778 break
779 else:
780 text_candidate_subs[k] = str(v)
781 if not match:
782 continue
783 # WSGI provides decoded URLs, without %xx escapes, and the URL
784 # resolver operates on such URLs. First substitute arguments
785 # without quoting to build a decoded URL and look for a match.
786 # Then, if we have a match, redo the substitution with quoted
787 # arguments in order to return a properly encoded URL.
788 candidate_pat = _prefix.replace("%", "%%") + result
789 if re.search(
790 "^%s%s" % (re.escape(_prefix), pattern),
791 candidate_pat % text_candidate_subs,
792 ):
793 # safe characters from `pchar` definition of RFC 3986
794 url = quote(
795 candidate_pat % text_candidate_subs,
796 safe=RFC3986_SUBDELIMS + "/~:@",
797 )
798 # Don't allow construction of scheme relative urls.
799 return escape_leading_slashes(url)
800 # lookup_view can be URL name or callable, but callables are not
801 # friendly in error messages.
802 m = getattr(lookup_view, "__module__", None)
803 n = getattr(lookup_view, "__name__", None)
804 if m is not None and n is not None:
805 lookup_view_s = "%s.%s" % (m, n)
806 else:
807 lookup_view_s = lookup_view
809 patterns = [pattern for (_, pattern, _, _) in possibilities]
810 if patterns:
811 if args:
812 arg_msg = "arguments '%s'" % (args,)
813 elif kwargs:
814 arg_msg = "keyword arguments '%s'" % kwargs
815 else:
816 arg_msg = "no arguments"
817 msg = "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" % (
818 lookup_view_s,
819 arg_msg,
820 len(patterns),
821 patterns,
822 )
823 else:
824 msg = (
825 "Reverse for '%(view)s' not found. '%(view)s' is not "
826 "a valid view function or pattern name." % {"view": lookup_view_s}
827 )
828 raise NoReverseMatch(msg)