Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

resolvers.py 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. """
  2. This module converts requested URLs to callback view functions.
  3. URLResolver is the main class here. Its resolve() method takes a URL (as
  4. a string) and returns a ResolverMatch object which provides access to all
  5. attributes of the resolved URL match.
  6. """
  7. import functools
  8. import re
  9. import threading
  10. from importlib import import_module
  11. from urllib.parse import quote
  12. from django.conf import settings
  13. from django.core.checks import Warning
  14. from django.core.checks.urls import check_resolver
  15. from django.core.exceptions import ImproperlyConfigured
  16. from django.utils.datastructures import MultiValueDict
  17. from django.utils.functional import cached_property
  18. from django.utils.http import RFC3986_SUBDELIMS, escape_leading_slashes
  19. from django.utils.regex_helper import normalize
  20. from django.utils.translation import get_language
  21. from .converters import get_converter
  22. from .exceptions import NoReverseMatch, Resolver404
  23. from .utils import get_callable
  24. class ResolverMatch:
  25. def __init__(self, func, args, kwargs, url_name=None, app_names=None, namespaces=None):
  26. self.func = func
  27. self.args = args
  28. self.kwargs = kwargs
  29. self.url_name = url_name
  30. # If a URLRegexResolver doesn't have a namespace or app_name, it passes
  31. # in an empty value.
  32. self.app_names = [x for x in app_names if x] if app_names else []
  33. self.app_name = ':'.join(self.app_names)
  34. self.namespaces = [x for x in namespaces if x] if namespaces else []
  35. self.namespace = ':'.join(self.namespaces)
  36. if not hasattr(func, '__name__'):
  37. # A class-based view
  38. self._func_path = func.__class__.__module__ + '.' + func.__class__.__name__
  39. else:
  40. # A function-based view
  41. self._func_path = func.__module__ + '.' + func.__name__
  42. view_path = url_name or self._func_path
  43. self.view_name = ':'.join(self.namespaces + [view_path])
  44. def __getitem__(self, index):
  45. return (self.func, self.args, self.kwargs)[index]
  46. def __repr__(self):
  47. return "ResolverMatch(func=%s, args=%s, kwargs=%s, url_name=%s, app_names=%s, namespaces=%s)" % (
  48. self._func_path, self.args, self.kwargs, self.url_name,
  49. self.app_names, self.namespaces,
  50. )
  51. @functools.lru_cache(maxsize=None)
  52. def get_resolver(urlconf=None):
  53. if urlconf is None:
  54. urlconf = settings.ROOT_URLCONF
  55. return URLResolver(RegexPattern(r'^/'), urlconf)
  56. @functools.lru_cache(maxsize=None)
  57. def get_ns_resolver(ns_pattern, resolver, converters):
  58. # Build a namespaced resolver for the given parent URLconf pattern.
  59. # This makes it possible to have captured parameters in the parent
  60. # URLconf pattern.
  61. pattern = RegexPattern(ns_pattern)
  62. pattern.converters = dict(converters)
  63. ns_resolver = URLResolver(pattern, resolver.url_patterns)
  64. return URLResolver(RegexPattern(r'^/'), [ns_resolver])
  65. class LocaleRegexDescriptor:
  66. def __init__(self, attr):
  67. self.attr = attr
  68. def __get__(self, instance, cls=None):
  69. """
  70. Return a compiled regular expression based on the active language.
  71. """
  72. if instance is None:
  73. return self
  74. # As a performance optimization, if the given regex string is a regular
  75. # string (not a lazily-translated string proxy), compile it once and
  76. # avoid per-language compilation.
  77. pattern = getattr(instance, self.attr)
  78. if isinstance(pattern, str):
  79. instance.__dict__['regex'] = instance._compile(pattern)
  80. return instance.__dict__['regex']
  81. language_code = get_language()
  82. if language_code not in instance._regex_dict:
  83. instance._regex_dict[language_code] = instance._compile(str(pattern))
  84. return instance._regex_dict[language_code]
  85. class CheckURLMixin:
  86. def describe(self):
  87. """
  88. Format the URL pattern for display in warning messages.
  89. """
  90. description = "'{}'".format(self)
  91. if self.name:
  92. description += " [name='{}']".format(self.name)
  93. return description
  94. def _check_pattern_startswith_slash(self):
  95. """
  96. Check that the pattern does not begin with a forward slash.
  97. """
  98. regex_pattern = self.regex.pattern
  99. if not settings.APPEND_SLASH:
  100. # Skip check as it can be useful to start a URL pattern with a slash
  101. # when APPEND_SLASH=False.
  102. return []
  103. if regex_pattern.startswith(('/', '^/', '^\\/')) and not regex_pattern.endswith('/'):
  104. warning = Warning(
  105. "Your URL pattern {} has a route beginning with a '/'. Remove this "
  106. "slash as it is unnecessary. If this pattern is targeted in an "
  107. "include(), ensure the include() pattern has a trailing '/'.".format(
  108. self.describe()
  109. ),
  110. id="urls.W002",
  111. )
  112. return [warning]
  113. else:
  114. return []
  115. class RegexPattern(CheckURLMixin):
  116. regex = LocaleRegexDescriptor('_regex')
  117. def __init__(self, regex, name=None, is_endpoint=False):
  118. self._regex = regex
  119. self._regex_dict = {}
  120. self._is_endpoint = is_endpoint
  121. self.name = name
  122. self.converters = {}
  123. def match(self, path):
  124. match = self.regex.search(path)
  125. if match:
  126. # If there are any named groups, use those as kwargs, ignoring
  127. # non-named groups. Otherwise, pass all non-named arguments as
  128. # positional arguments.
  129. kwargs = match.groupdict()
  130. args = () if kwargs else match.groups()
  131. return path[match.end():], args, kwargs
  132. return None
  133. def check(self):
  134. warnings = []
  135. warnings.extend(self._check_pattern_startswith_slash())
  136. if not self._is_endpoint:
  137. warnings.extend(self._check_include_trailing_dollar())
  138. return warnings
  139. def _check_include_trailing_dollar(self):
  140. regex_pattern = self.regex.pattern
  141. if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
  142. return [Warning(
  143. "Your URL pattern {} uses include with a route ending with a '$'. "
  144. "Remove the dollar from the route to avoid problems including "
  145. "URLs.".format(self.describe()),
  146. id='urls.W001',
  147. )]
  148. else:
  149. return []
  150. def _compile(self, regex):
  151. """Compile and return the given regular expression."""
  152. try:
  153. return re.compile(regex)
  154. except re.error as e:
  155. raise ImproperlyConfigured(
  156. '"%s" is not a valid regular expression: %s' % (regex, e)
  157. )
  158. def __str__(self):
  159. return str(self._regex)
  160. _PATH_PARAMETER_COMPONENT_RE = re.compile(
  161. r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
  162. )
  163. def _route_to_regex(route, is_endpoint=False):
  164. """
  165. Convert a path pattern into a regular expression. Return the regular
  166. expression and a dictionary mapping the capture names to the converters.
  167. For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
  168. and {'pk': <django.urls.converters.IntConverter>}.
  169. """
  170. original_route = route
  171. parts = ['^']
  172. converters = {}
  173. while True:
  174. match = _PATH_PARAMETER_COMPONENT_RE.search(route)
  175. if not match:
  176. parts.append(re.escape(route))
  177. break
  178. parts.append(re.escape(route[:match.start()]))
  179. route = route[match.end():]
  180. parameter = match.group('parameter')
  181. if not parameter.isidentifier():
  182. raise ImproperlyConfigured(
  183. "URL route '%s' uses parameter name %r which isn't a valid "
  184. "Python identifier." % (original_route, parameter)
  185. )
  186. raw_converter = match.group('converter')
  187. if raw_converter is None:
  188. # If a converter isn't specified, the default is `str`.
  189. raw_converter = 'str'
  190. try:
  191. converter = get_converter(raw_converter)
  192. except KeyError as e:
  193. raise ImproperlyConfigured(
  194. "URL route '%s' uses invalid converter %s." % (original_route, e)
  195. )
  196. converters[parameter] = converter
  197. parts.append('(?P<' + parameter + '>' + converter.regex + ')')
  198. if is_endpoint:
  199. parts.append('$')
  200. return ''.join(parts), converters
  201. class RoutePattern(CheckURLMixin):
  202. regex = LocaleRegexDescriptor('_route')
  203. def __init__(self, route, name=None, is_endpoint=False):
  204. self._route = route
  205. self._regex_dict = {}
  206. self._is_endpoint = is_endpoint
  207. self.name = name
  208. self.converters = _route_to_regex(str(route), is_endpoint)[1]
  209. def match(self, path):
  210. match = self.regex.search(path)
  211. if match:
  212. # RoutePattern doesn't allow non-named groups so args are ignored.
  213. kwargs = match.groupdict()
  214. for key, value in kwargs.items():
  215. converter = self.converters[key]
  216. try:
  217. kwargs[key] = converter.to_python(value)
  218. except ValueError:
  219. return None
  220. return path[match.end():], (), kwargs
  221. return None
  222. def check(self):
  223. warnings = self._check_pattern_startswith_slash()
  224. route = self._route
  225. if '(?P<' in route or route.startswith('^') or route.endswith('$'):
  226. warnings.append(Warning(
  227. "Your URL pattern {} has a route that contains '(?P<', begins "
  228. "with a '^', or ends with a '$'. This was likely an oversight "
  229. "when migrating to django.urls.path().".format(self.describe()),
  230. id='2_0.W001',
  231. ))
  232. return warnings
  233. def _compile(self, route):
  234. return re.compile(_route_to_regex(route, self._is_endpoint)[0])
  235. def __str__(self):
  236. return str(self._route)
  237. class LocalePrefixPattern:
  238. def __init__(self, prefix_default_language=True):
  239. self.prefix_default_language = prefix_default_language
  240. self.converters = {}
  241. @property
  242. def regex(self):
  243. # This is only used by reverse() and cached in _reverse_dict.
  244. return re.compile(self.language_prefix)
  245. @property
  246. def language_prefix(self):
  247. language_code = get_language() or settings.LANGUAGE_CODE
  248. if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
  249. return ''
  250. else:
  251. return '%s/' % language_code
  252. def match(self, path):
  253. language_prefix = self.language_prefix
  254. if path.startswith(language_prefix):
  255. return path[len(language_prefix):], (), {}
  256. return None
  257. def check(self):
  258. return []
  259. def describe(self):
  260. return "'{}'".format(self)
  261. def __str__(self):
  262. return self.language_prefix
  263. class URLPattern:
  264. def __init__(self, pattern, callback, default_args=None, name=None):
  265. self.pattern = pattern
  266. self.callback = callback # the view
  267. self.default_args = default_args or {}
  268. self.name = name
  269. def __repr__(self):
  270. return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
  271. def check(self):
  272. warnings = self._check_pattern_name()
  273. warnings.extend(self.pattern.check())
  274. return warnings
  275. def _check_pattern_name(self):
  276. """
  277. Check that the pattern name does not contain a colon.
  278. """
  279. if self.pattern.name is not None and ":" in self.pattern.name:
  280. warning = Warning(
  281. "Your URL pattern {} has a name including a ':'. Remove the colon, to "
  282. "avoid ambiguous namespace references.".format(self.pattern.describe()),
  283. id="urls.W003",
  284. )
  285. return [warning]
  286. else:
  287. return []
  288. def resolve(self, path):
  289. match = self.pattern.match(path)
  290. if match:
  291. new_path, args, kwargs = match
  292. # Pass any extra_kwargs as **kwargs.
  293. kwargs.update(self.default_args)
  294. return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
  295. @cached_property
  296. def lookup_str(self):
  297. """
  298. A string that identifies the view (e.g. 'path.to.view_function' or
  299. 'path.to.ClassBasedView').
  300. """
  301. callback = self.callback
  302. if isinstance(callback, functools.partial):
  303. callback = callback.func
  304. if not hasattr(callback, '__name__'):
  305. return callback.__module__ + "." + callback.__class__.__name__
  306. return callback.__module__ + "." + callback.__qualname__
  307. class URLResolver:
  308. def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
  309. self.pattern = pattern
  310. # urlconf_name is the dotted Python path to the module defining
  311. # urlpatterns. It may also be an object with an urlpatterns attribute
  312. # or urlpatterns itself.
  313. self.urlconf_name = urlconf_name
  314. self.callback = None
  315. self.default_kwargs = default_kwargs or {}
  316. self.namespace = namespace
  317. self.app_name = app_name
  318. self._reverse_dict = {}
  319. self._namespace_dict = {}
  320. self._app_dict = {}
  321. # set of dotted paths to all functions and classes that are used in
  322. # urlpatterns
  323. self._callback_strs = set()
  324. self._populated = False
  325. self._local = threading.local()
  326. def __repr__(self):
  327. if isinstance(self.urlconf_name, list) and self.urlconf_name:
  328. # Don't bother to output the whole list, it can be huge
  329. urlconf_repr = '<%s list>' % self.urlconf_name[0].__class__.__name__
  330. else:
  331. urlconf_repr = repr(self.urlconf_name)
  332. return '<%s %s (%s:%s) %s>' % (
  333. self.__class__.__name__, urlconf_repr, self.app_name,
  334. self.namespace, self.pattern.describe(),
  335. )
  336. def check(self):
  337. warnings = []
  338. for pattern in self.url_patterns:
  339. warnings.extend(check_resolver(pattern))
  340. return warnings or self.pattern.check()
  341. def _populate(self):
  342. # Short-circuit if called recursively in this thread to prevent
  343. # infinite recursion. Concurrent threads may call this at the same
  344. # time and will need to continue, so set 'populating' on a
  345. # thread-local variable.
  346. if getattr(self._local, 'populating', False):
  347. return
  348. try:
  349. self._local.populating = True
  350. lookups = MultiValueDict()
  351. namespaces = {}
  352. apps = {}
  353. language_code = get_language()
  354. for url_pattern in reversed(self.url_patterns):
  355. p_pattern = url_pattern.pattern.regex.pattern
  356. if p_pattern.startswith('^'):
  357. p_pattern = p_pattern[1:]
  358. if isinstance(url_pattern, URLPattern):
  359. self._callback_strs.add(url_pattern.lookup_str)
  360. bits = normalize(url_pattern.pattern.regex.pattern)
  361. lookups.appendlist(
  362. url_pattern.callback,
  363. (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
  364. )
  365. if url_pattern.name is not None:
  366. lookups.appendlist(
  367. url_pattern.name,
  368. (bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
  369. )
  370. else: # url_pattern is a URLResolver.
  371. url_pattern._populate()
  372. if url_pattern.app_name:
  373. apps.setdefault(url_pattern.app_name, []).append(url_pattern.namespace)
  374. namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
  375. else:
  376. for name in url_pattern.reverse_dict:
  377. for matches, pat, defaults, converters in url_pattern.reverse_dict.getlist(name):
  378. new_matches = normalize(p_pattern + pat)
  379. lookups.appendlist(
  380. name,
  381. (
  382. new_matches,
  383. p_pattern + pat,
  384. {**defaults, **url_pattern.default_kwargs},
  385. {**self.pattern.converters, **url_pattern.pattern.converters, **converters}
  386. )
  387. )
  388. for namespace, (prefix, sub_pattern) in url_pattern.namespace_dict.items():
  389. namespaces[namespace] = (p_pattern + prefix, sub_pattern)
  390. for app_name, namespace_list in url_pattern.app_dict.items():
  391. apps.setdefault(app_name, []).extend(namespace_list)
  392. self._callback_strs.update(url_pattern._callback_strs)
  393. self._namespace_dict[language_code] = namespaces
  394. self._app_dict[language_code] = apps
  395. self._reverse_dict[language_code] = lookups
  396. self._populated = True
  397. finally:
  398. self._local.populating = False
  399. @property
  400. def reverse_dict(self):
  401. language_code = get_language()
  402. if language_code not in self._reverse_dict:
  403. self._populate()
  404. return self._reverse_dict[language_code]
  405. @property
  406. def namespace_dict(self):
  407. language_code = get_language()
  408. if language_code not in self._namespace_dict:
  409. self._populate()
  410. return self._namespace_dict[language_code]
  411. @property
  412. def app_dict(self):
  413. language_code = get_language()
  414. if language_code not in self._app_dict:
  415. self._populate()
  416. return self._app_dict[language_code]
  417. def _is_callback(self, name):
  418. if not self._populated:
  419. self._populate()
  420. return name in self._callback_strs
  421. def resolve(self, path):
  422. path = str(path) # path may be a reverse_lazy object
  423. tried = []
  424. match = self.pattern.match(path)
  425. if match:
  426. new_path, args, kwargs = match
  427. for pattern in self.url_patterns:
  428. try:
  429. sub_match = pattern.resolve(new_path)
  430. except Resolver404 as e:
  431. sub_tried = e.args[0].get('tried')
  432. if sub_tried is not None:
  433. tried.extend([pattern] + t for t in sub_tried)
  434. else:
  435. tried.append([pattern])
  436. else:
  437. if sub_match:
  438. # Merge captured arguments in match with submatch
  439. sub_match_dict = {**kwargs, **self.default_kwargs}
  440. # Update the sub_match_dict with the kwargs from the sub_match.
  441. sub_match_dict.update(sub_match.kwargs)
  442. # If there are *any* named groups, ignore all non-named groups.
  443. # Otherwise, pass all non-named arguments as positional arguments.
  444. sub_match_args = sub_match.args
  445. if not sub_match_dict:
  446. sub_match_args = args + sub_match.args
  447. return ResolverMatch(
  448. sub_match.func,
  449. sub_match_args,
  450. sub_match_dict,
  451. sub_match.url_name,
  452. [self.app_name] + sub_match.app_names,
  453. [self.namespace] + sub_match.namespaces,
  454. )
  455. tried.append([pattern])
  456. raise Resolver404({'tried': tried, 'path': new_path})
  457. raise Resolver404({'path': path})
  458. @cached_property
  459. def urlconf_module(self):
  460. if isinstance(self.urlconf_name, str):
  461. return import_module(self.urlconf_name)
  462. else:
  463. return self.urlconf_name
  464. @cached_property
  465. def url_patterns(self):
  466. # urlconf_module might be a valid set of patterns, so we default to it
  467. patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  468. try:
  469. iter(patterns)
  470. except TypeError:
  471. msg = (
  472. "The included URLconf '{name}' does not appear to have any "
  473. "patterns in it. If you see valid patterns in the file then "
  474. "the issue is probably caused by a circular import."
  475. )
  476. raise ImproperlyConfigured(msg.format(name=self.urlconf_name))
  477. return patterns
  478. def resolve_error_handler(self, view_type):
  479. callback = getattr(self.urlconf_module, 'handler%s' % view_type, None)
  480. if not callback:
  481. # No handler specified in file; use lazy import, since
  482. # django.conf.urls imports this file.
  483. from django.conf import urls
  484. callback = getattr(urls, 'handler%s' % view_type)
  485. return get_callable(callback), {}
  486. def reverse(self, lookup_view, *args, **kwargs):
  487. return self._reverse_with_prefix(lookup_view, '', *args, **kwargs)
  488. def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
  489. if args and kwargs:
  490. raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
  491. if not self._populated:
  492. self._populate()
  493. possibilities = self.reverse_dict.getlist(lookup_view)
  494. for possibility, pattern, defaults, converters in possibilities:
  495. for result, params in possibility:
  496. if args:
  497. if len(args) != len(params):
  498. continue
  499. candidate_subs = dict(zip(params, args))
  500. else:
  501. if set(kwargs).symmetric_difference(params).difference(defaults):
  502. continue
  503. if any(kwargs.get(k, v) != v for k, v in defaults.items()):
  504. continue
  505. candidate_subs = kwargs
  506. # Convert the candidate subs to text using Converter.to_url().
  507. text_candidate_subs = {}
  508. for k, v in candidate_subs.items():
  509. if k in converters:
  510. text_candidate_subs[k] = converters[k].to_url(v)
  511. else:
  512. text_candidate_subs[k] = str(v)
  513. # WSGI provides decoded URLs, without %xx escapes, and the URL
  514. # resolver operates on such URLs. First substitute arguments
  515. # without quoting to build a decoded URL and look for a match.
  516. # Then, if we have a match, redo the substitution with quoted
  517. # arguments in order to return a properly encoded URL.
  518. candidate_pat = _prefix.replace('%', '%%') + result
  519. if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
  520. # safe characters from `pchar` definition of RFC 3986
  521. url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
  522. # Don't allow construction of scheme relative urls.
  523. return escape_leading_slashes(url)
  524. # lookup_view can be URL name or callable, but callables are not
  525. # friendly in error messages.
  526. m = getattr(lookup_view, '__module__', None)
  527. n = getattr(lookup_view, '__name__', None)
  528. if m is not None and n is not None:
  529. lookup_view_s = "%s.%s" % (m, n)
  530. else:
  531. lookup_view_s = lookup_view
  532. patterns = [pattern for (_, pattern, _, _) in possibilities]
  533. if patterns:
  534. if args:
  535. arg_msg = "arguments '%s'" % (args,)
  536. elif kwargs:
  537. arg_msg = "keyword arguments '%s'" % (kwargs,)
  538. else:
  539. arg_msg = "no arguments"
  540. msg = (
  541. "Reverse for '%s' with %s not found. %d pattern(s) tried: %s" %
  542. (lookup_view_s, arg_msg, len(patterns), patterns)
  543. )
  544. else:
  545. msg = (
  546. "Reverse for '%(view)s' not found. '%(view)s' is not "
  547. "a valid view function or pattern name." % {'view': lookup_view_s}
  548. )
  549. raise NoReverseMatch(msg)