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