rules.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. import ast
  2. import re
  3. import typing as t
  4. from dataclasses import dataclass
  5. from string import Template
  6. from types import CodeType
  7. from .._internal import _to_bytes
  8. from ..urls import url_encode
  9. from ..urls import url_quote
  10. from .converters import ValidationError
  11. if t.TYPE_CHECKING:
  12. from .converters import BaseConverter
  13. from .map import Map
  14. class Weighting(t.NamedTuple):
  15. number_static_weights: int
  16. static_weights: t.List[t.Tuple[int, int]]
  17. number_argument_weights: int
  18. argument_weights: t.List[int]
  19. @dataclass
  20. class RulePart:
  21. """A part of a rule.
  22. Rules can be represented by parts as delimited by `/` with
  23. instances of this class representing those parts. The *content* is
  24. either the raw content if *static* or a regex string to match
  25. against. The *weight* can be used to order parts when matching.
  26. """
  27. content: str
  28. final: bool
  29. static: bool
  30. weight: Weighting
  31. _part_re = re.compile(
  32. r"""
  33. (?:
  34. (?P<slash>\/) # a slash
  35. |
  36. (?P<static>[^<\/]+) # static rule data
  37. |
  38. (?:
  39. <
  40. (?:
  41. (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
  42. (?:\((?P<arguments>.*?)\))? # converter arguments
  43. \: # variable delimiter
  44. )?
  45. (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
  46. >
  47. )
  48. )
  49. """,
  50. re.VERBOSE,
  51. )
  52. _simple_rule_re = re.compile(r"<([^>]+)>")
  53. _converter_args_re = re.compile(
  54. r"""
  55. ((?P<name>\w+)\s*=\s*)?
  56. (?P<value>
  57. True|False|
  58. \d+.\d+|
  59. \d+.|
  60. \d+|
  61. [\w\d_.]+|
  62. [urUR]?(?P<stringval>"[^"]*?"|'[^']*')
  63. )\s*,
  64. """,
  65. re.VERBOSE,
  66. )
  67. _PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
  68. def _find(value: str, target: str, pos: int) -> int:
  69. """Find the *target* in *value* after *pos*.
  70. Returns the *value* length if *target* isn't found.
  71. """
  72. try:
  73. return value.index(target, pos)
  74. except ValueError:
  75. return len(value)
  76. def _pythonize(value: str) -> t.Union[None, bool, int, float, str]:
  77. if value in _PYTHON_CONSTANTS:
  78. return _PYTHON_CONSTANTS[value]
  79. for convert in int, float:
  80. try:
  81. return convert(value) # type: ignore
  82. except ValueError:
  83. pass
  84. if value[:1] == value[-1:] and value[0] in "\"'":
  85. value = value[1:-1]
  86. return str(value)
  87. def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]:
  88. argstr += ","
  89. args = []
  90. kwargs = {}
  91. for item in _converter_args_re.finditer(argstr):
  92. value = item.group("stringval")
  93. if value is None:
  94. value = item.group("value")
  95. value = _pythonize(value)
  96. if not item.group("name"):
  97. args.append(value)
  98. else:
  99. name = item.group("name")
  100. kwargs[name] = value
  101. return tuple(args), kwargs
  102. class RuleFactory:
  103. """As soon as you have more complex URL setups it's a good idea to use rule
  104. factories to avoid repetitive tasks. Some of them are builtin, others can
  105. be added by subclassing `RuleFactory` and overriding `get_rules`.
  106. """
  107. def get_rules(self, map: "Map") -> t.Iterable["Rule"]:
  108. """Subclasses of `RuleFactory` have to override this method and return
  109. an iterable of rules."""
  110. raise NotImplementedError()
  111. class Subdomain(RuleFactory):
  112. """All URLs provided by this factory have the subdomain set to a
  113. specific domain. For example if you want to use the subdomain for
  114. the current language this can be a good setup::
  115. url_map = Map([
  116. Rule('/', endpoint='#select_language'),
  117. Subdomain('<string(length=2):lang_code>', [
  118. Rule('/', endpoint='index'),
  119. Rule('/about', endpoint='about'),
  120. Rule('/help', endpoint='help')
  121. ])
  122. ])
  123. All the rules except for the ``'#select_language'`` endpoint will now
  124. listen on a two letter long subdomain that holds the language code
  125. for the current request.
  126. """
  127. def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
  128. self.subdomain = subdomain
  129. self.rules = rules
  130. def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
  131. for rulefactory in self.rules:
  132. for rule in rulefactory.get_rules(map):
  133. rule = rule.empty()
  134. rule.subdomain = self.subdomain
  135. yield rule
  136. class Submount(RuleFactory):
  137. """Like `Subdomain` but prefixes the URL rule with a given string::
  138. url_map = Map([
  139. Rule('/', endpoint='index'),
  140. Submount('/blog', [
  141. Rule('/', endpoint='blog/index'),
  142. Rule('/entry/<entry_slug>', endpoint='blog/show')
  143. ])
  144. ])
  145. Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
  146. """
  147. def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
  148. self.path = path.rstrip("/")
  149. self.rules = rules
  150. def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
  151. for rulefactory in self.rules:
  152. for rule in rulefactory.get_rules(map):
  153. rule = rule.empty()
  154. rule.rule = self.path + rule.rule
  155. yield rule
  156. class EndpointPrefix(RuleFactory):
  157. """Prefixes all endpoints (which must be strings for this factory) with
  158. another string. This can be useful for sub applications::
  159. url_map = Map([
  160. Rule('/', endpoint='index'),
  161. EndpointPrefix('blog/', [Submount('/blog', [
  162. Rule('/', endpoint='index'),
  163. Rule('/entry/<entry_slug>', endpoint='show')
  164. ])])
  165. ])
  166. """
  167. def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
  168. self.prefix = prefix
  169. self.rules = rules
  170. def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
  171. for rulefactory in self.rules:
  172. for rule in rulefactory.get_rules(map):
  173. rule = rule.empty()
  174. rule.endpoint = self.prefix + rule.endpoint
  175. yield rule
  176. class RuleTemplate:
  177. """Returns copies of the rules wrapped and expands string templates in
  178. the endpoint, rule, defaults or subdomain sections.
  179. Here a small example for such a rule template::
  180. from werkzeug.routing import Map, Rule, RuleTemplate
  181. resource = RuleTemplate([
  182. Rule('/$name/', endpoint='$name.list'),
  183. Rule('/$name/<int:id>', endpoint='$name.show')
  184. ])
  185. url_map = Map([resource(name='user'), resource(name='page')])
  186. When a rule template is called the keyword arguments are used to
  187. replace the placeholders in all the string parameters.
  188. """
  189. def __init__(self, rules: t.Iterable["Rule"]) -> None:
  190. self.rules = list(rules)
  191. def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory":
  192. return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
  193. class RuleTemplateFactory(RuleFactory):
  194. """A factory that fills in template variables into rules. Used by
  195. `RuleTemplate` internally.
  196. :internal:
  197. """
  198. def __init__(
  199. self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any]
  200. ) -> None:
  201. self.rules = rules
  202. self.context = context
  203. def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
  204. for rulefactory in self.rules:
  205. for rule in rulefactory.get_rules(map):
  206. new_defaults = subdomain = None
  207. if rule.defaults:
  208. new_defaults = {}
  209. for key, value in rule.defaults.items():
  210. if isinstance(value, str):
  211. value = Template(value).substitute(self.context)
  212. new_defaults[key] = value
  213. if rule.subdomain is not None:
  214. subdomain = Template(rule.subdomain).substitute(self.context)
  215. new_endpoint = rule.endpoint
  216. if isinstance(new_endpoint, str):
  217. new_endpoint = Template(new_endpoint).substitute(self.context)
  218. yield Rule(
  219. Template(rule.rule).substitute(self.context),
  220. new_defaults,
  221. subdomain,
  222. rule.methods,
  223. rule.build_only,
  224. new_endpoint,
  225. rule.strict_slashes,
  226. )
  227. def _prefix_names(src: str) -> ast.stmt:
  228. """ast parse and prefix names with `.` to avoid collision with user vars"""
  229. tree = ast.parse(src).body[0]
  230. if isinstance(tree, ast.Expr):
  231. tree = tree.value # type: ignore
  232. for node in ast.walk(tree):
  233. if isinstance(node, ast.Name):
  234. node.id = f".{node.id}"
  235. return tree
  236. _CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
  237. _IF_KWARGS_URL_ENCODE_CODE = """\
  238. if kwargs:
  239. params = self._encode_query_vars(kwargs)
  240. q = "?" if params else ""
  241. else:
  242. q = params = ""
  243. """
  244. _IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE)
  245. _URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params"))
  246. class Rule(RuleFactory):
  247. """A Rule represents one URL pattern. There are some options for `Rule`
  248. that change the way it behaves and are passed to the `Rule` constructor.
  249. Note that besides the rule-string all arguments *must* be keyword arguments
  250. in order to not break the application on Werkzeug upgrades.
  251. `string`
  252. Rule strings basically are just normal URL paths with placeholders in
  253. the format ``<converter(arguments):name>`` where the converter and the
  254. arguments are optional. If no converter is defined the `default`
  255. converter is used which means `string` in the normal configuration.
  256. URL rules that end with a slash are branch URLs, others are leaves.
  257. If you have `strict_slashes` enabled (which is the default), all
  258. branch URLs that are matched without a trailing slash will trigger a
  259. redirect to the same URL with the missing slash appended.
  260. The converters are defined on the `Map`.
  261. `endpoint`
  262. The endpoint for this rule. This can be anything. A reference to a
  263. function, a string, a number etc. The preferred way is using a string
  264. because the endpoint is used for URL generation.
  265. `defaults`
  266. An optional dict with defaults for other rules with the same endpoint.
  267. This is a bit tricky but useful if you want to have unique URLs::
  268. url_map = Map([
  269. Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
  270. Rule('/all/page/<int:page>', endpoint='all_entries')
  271. ])
  272. If a user now visits ``http://example.com/all/page/1`` they will be
  273. redirected to ``http://example.com/all/``. If `redirect_defaults` is
  274. disabled on the `Map` instance this will only affect the URL
  275. generation.
  276. `subdomain`
  277. The subdomain rule string for this rule. If not specified the rule
  278. only matches for the `default_subdomain` of the map. If the map is
  279. not bound to a subdomain this feature is disabled.
  280. Can be useful if you want to have user profiles on different subdomains
  281. and all subdomains are forwarded to your application::
  282. url_map = Map([
  283. Rule('/', subdomain='<username>', endpoint='user/homepage'),
  284. Rule('/stats', subdomain='<username>', endpoint='user/stats')
  285. ])
  286. `methods`
  287. A sequence of http methods this rule applies to. If not specified, all
  288. methods are allowed. For example this can be useful if you want different
  289. endpoints for `POST` and `GET`. If methods are defined and the path
  290. matches but the method matched against is not in this list or in the
  291. list of another rule for that path the error raised is of the type
  292. `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the
  293. list of methods and `HEAD` is not, `HEAD` is added automatically.
  294. `strict_slashes`
  295. Override the `Map` setting for `strict_slashes` only for this rule. If
  296. not specified the `Map` setting is used.
  297. `merge_slashes`
  298. Override :attr:`Map.merge_slashes` for this rule.
  299. `build_only`
  300. Set this to True and the rule will never match but will create a URL
  301. that can be build. This is useful if you have resources on a subdomain
  302. or folder that are not handled by the WSGI application (like static data)
  303. `redirect_to`
  304. If given this must be either a string or callable. In case of a
  305. callable it's called with the url adapter that triggered the match and
  306. the values of the URL as keyword arguments and has to return the target
  307. for the redirect, otherwise it has to be a string with placeholders in
  308. rule syntax::
  309. def foo_with_slug(adapter, id):
  310. # ask the database for the slug for the old id. this of
  311. # course has nothing to do with werkzeug.
  312. return f'foo/{Foo.get_slug_for_id(id)}'
  313. url_map = Map([
  314. Rule('/foo/<slug>', endpoint='foo'),
  315. Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
  316. Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug)
  317. ])
  318. When the rule is matched the routing system will raise a
  319. `RequestRedirect` exception with the target for the redirect.
  320. Keep in mind that the URL will be joined against the URL root of the
  321. script so don't use a leading slash on the target URL unless you
  322. really mean root of that domain.
  323. `alias`
  324. If enabled this rule serves as an alias for another rule with the same
  325. endpoint and arguments.
  326. `host`
  327. If provided and the URL map has host matching enabled this can be
  328. used to provide a match rule for the whole host. This also means
  329. that the subdomain feature is disabled.
  330. `websocket`
  331. If ``True``, this rule is only matches for WebSocket (``ws://``,
  332. ``wss://``) requests. By default, rules will only match for HTTP
  333. requests.
  334. .. versionchanged:: 2.1
  335. Percent-encoded newlines (``%0a``), which are decoded by WSGI
  336. servers, are considered when routing instead of terminating the
  337. match early.
  338. .. versionadded:: 1.0
  339. Added ``websocket``.
  340. .. versionadded:: 1.0
  341. Added ``merge_slashes``.
  342. .. versionadded:: 0.7
  343. Added ``alias`` and ``host``.
  344. .. versionchanged:: 0.6.1
  345. ``HEAD`` is added to ``methods`` if ``GET`` is present.
  346. """
  347. def __init__(
  348. self,
  349. string: str,
  350. defaults: t.Optional[t.Mapping[str, t.Any]] = None,
  351. subdomain: t.Optional[str] = None,
  352. methods: t.Optional[t.Iterable[str]] = None,
  353. build_only: bool = False,
  354. endpoint: t.Optional[str] = None,
  355. strict_slashes: t.Optional[bool] = None,
  356. merge_slashes: t.Optional[bool] = None,
  357. redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None,
  358. alias: bool = False,
  359. host: t.Optional[str] = None,
  360. websocket: bool = False,
  361. ) -> None:
  362. if not string.startswith("/"):
  363. raise ValueError("urls must start with a leading slash")
  364. self.rule = string
  365. self.is_leaf = not string.endswith("/")
  366. self.is_branch = string.endswith("/")
  367. self.map: "Map" = None # type: ignore
  368. self.strict_slashes = strict_slashes
  369. self.merge_slashes = merge_slashes
  370. self.subdomain = subdomain
  371. self.host = host
  372. self.defaults = defaults
  373. self.build_only = build_only
  374. self.alias = alias
  375. self.websocket = websocket
  376. if methods is not None:
  377. if isinstance(methods, str):
  378. raise TypeError("'methods' should be a list of strings.")
  379. methods = {x.upper() for x in methods}
  380. if "HEAD" not in methods and "GET" in methods:
  381. methods.add("HEAD")
  382. if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
  383. raise ValueError(
  384. "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
  385. )
  386. self.methods = methods
  387. self.endpoint: str = endpoint # type: ignore
  388. self.redirect_to = redirect_to
  389. if defaults:
  390. self.arguments = set(map(str, defaults))
  391. else:
  392. self.arguments = set()
  393. self._converters: t.Dict[str, "BaseConverter"] = {}
  394. self._trace: t.List[t.Tuple[bool, str]] = []
  395. self._parts: t.List[RulePart] = []
  396. def empty(self) -> "Rule":
  397. """
  398. Return an unbound copy of this rule.
  399. This can be useful if want to reuse an already bound URL for another
  400. map. See ``get_empty_kwargs`` to override what keyword arguments are
  401. provided to the new copy.
  402. """
  403. return type(self)(self.rule, **self.get_empty_kwargs())
  404. def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
  405. """
  406. Provides kwargs for instantiating empty copy with empty()
  407. Use this method to provide custom keyword arguments to the subclass of
  408. ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
  409. has custom keyword arguments that are needed at instantiation.
  410. Must return a ``dict`` that will be provided as kwargs to the new
  411. instance of ``Rule``, following the initial ``self.rule`` value which
  412. is always provided as the first, required positional argument.
  413. """
  414. defaults = None
  415. if self.defaults:
  416. defaults = dict(self.defaults)
  417. return dict(
  418. defaults=defaults,
  419. subdomain=self.subdomain,
  420. methods=self.methods,
  421. build_only=self.build_only,
  422. endpoint=self.endpoint,
  423. strict_slashes=self.strict_slashes,
  424. redirect_to=self.redirect_to,
  425. alias=self.alias,
  426. host=self.host,
  427. )
  428. def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
  429. yield self
  430. def refresh(self) -> None:
  431. """Rebinds and refreshes the URL. Call this if you modified the
  432. rule in place.
  433. :internal:
  434. """
  435. self.bind(self.map, rebind=True)
  436. def bind(self, map: "Map", rebind: bool = False) -> None:
  437. """Bind the url to a map and create a regular expression based on
  438. the information from the rule itself and the defaults from the map.
  439. :internal:
  440. """
  441. if self.map is not None and not rebind:
  442. raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
  443. self.map = map
  444. if self.strict_slashes is None:
  445. self.strict_slashes = map.strict_slashes
  446. if self.merge_slashes is None:
  447. self.merge_slashes = map.merge_slashes
  448. if self.subdomain is None:
  449. self.subdomain = map.default_subdomain
  450. self.compile()
  451. def get_converter(
  452. self,
  453. variable_name: str,
  454. converter_name: str,
  455. args: t.Tuple,
  456. kwargs: t.Mapping[str, t.Any],
  457. ) -> "BaseConverter":
  458. """Looks up the converter for the given parameter.
  459. .. versionadded:: 0.9
  460. """
  461. if converter_name not in self.map.converters:
  462. raise LookupError(f"the converter {converter_name!r} does not exist")
  463. return self.map.converters[converter_name](self.map, *args, **kwargs)
  464. def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
  465. return url_encode(
  466. query_vars,
  467. charset=self.map.charset,
  468. sort=self.map.sort_parameters,
  469. key=self.map.sort_key,
  470. )
  471. def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
  472. content = ""
  473. static = True
  474. argument_weights = []
  475. static_weights: t.List[t.Tuple[int, int]] = []
  476. final = False
  477. pos = 0
  478. while pos < len(rule):
  479. match = _part_re.match(rule, pos)
  480. if match is None:
  481. raise ValueError(f"malformed url rule: {rule!r}")
  482. data = match.groupdict()
  483. if data["static"] is not None:
  484. static_weights.append((len(static_weights), -len(data["static"])))
  485. self._trace.append((False, data["static"]))
  486. content += data["static"] if static else re.escape(data["static"])
  487. if data["variable"] is not None:
  488. if static:
  489. # Switching content to represent regex, hence the need to escape
  490. content = re.escape(content)
  491. static = False
  492. c_args, c_kwargs = parse_converter_args(data["arguments"] or "")
  493. convobj = self.get_converter(
  494. data["variable"], data["converter"] or "default", c_args, c_kwargs
  495. )
  496. self._converters[data["variable"]] = convobj
  497. self.arguments.add(data["variable"])
  498. if not convobj.part_isolating:
  499. final = True
  500. content += f"({convobj.regex})"
  501. argument_weights.append(convobj.weight)
  502. self._trace.append((True, data["variable"]))
  503. if data["slash"] is not None:
  504. self._trace.append((False, "/"))
  505. if final:
  506. content += "/"
  507. else:
  508. if not static:
  509. content += r"\Z"
  510. weight = Weighting(
  511. -len(static_weights),
  512. static_weights,
  513. -len(argument_weights),
  514. argument_weights,
  515. )
  516. yield RulePart(
  517. content=content, final=final, static=static, weight=weight
  518. )
  519. content = ""
  520. static = True
  521. argument_weights = []
  522. static_weights = []
  523. final = False
  524. pos = match.end()
  525. if not static:
  526. content += r"\Z"
  527. weight = Weighting(
  528. -len(static_weights),
  529. static_weights,
  530. -len(argument_weights),
  531. argument_weights,
  532. )
  533. yield RulePart(content=content, final=final, static=static, weight=weight)
  534. def compile(self) -> None:
  535. """Compiles the regular expression and stores it."""
  536. assert self.map is not None, "rule not bound"
  537. if self.map.host_matching:
  538. domain_rule = self.host or ""
  539. else:
  540. domain_rule = self.subdomain or ""
  541. self._parts = []
  542. self._trace = []
  543. self._converters = {}
  544. if domain_rule == "":
  545. self._parts = [
  546. RulePart(
  547. content="", final=False, static=True, weight=Weighting(0, [], 0, [])
  548. )
  549. ]
  550. else:
  551. self._parts.extend(self._parse_rule(domain_rule))
  552. self._trace.append((False, "|"))
  553. rule = self.rule
  554. if self.merge_slashes:
  555. rule = re.sub("/{2,}?", "/", self.rule)
  556. self._parts.extend(self._parse_rule(rule))
  557. self._build: t.Callable[..., t.Tuple[str, str]]
  558. self._build = self._compile_builder(False).__get__(self, None)
  559. self._build_unknown: t.Callable[..., t.Tuple[str, str]]
  560. self._build_unknown = self._compile_builder(True).__get__(self, None)
  561. @staticmethod
  562. def _get_func_code(code: CodeType, name: str) -> t.Callable[..., t.Tuple[str, str]]:
  563. globs: t.Dict[str, t.Any] = {}
  564. locs: t.Dict[str, t.Any] = {}
  565. exec(code, globs, locs)
  566. return locs[name] # type: ignore
  567. def _compile_builder(
  568. self, append_unknown: bool = True
  569. ) -> t.Callable[..., t.Tuple[str, str]]:
  570. defaults = self.defaults or {}
  571. dom_ops: t.List[t.Tuple[bool, str]] = []
  572. url_ops: t.List[t.Tuple[bool, str]] = []
  573. opl = dom_ops
  574. for is_dynamic, data in self._trace:
  575. if data == "|" and opl is dom_ops:
  576. opl = url_ops
  577. continue
  578. # this seems like a silly case to ever come up but:
  579. # if a default is given for a value that appears in the rule,
  580. # resolve it to a constant ahead of time
  581. if is_dynamic and data in defaults:
  582. data = self._converters[data].to_url(defaults[data])
  583. opl.append((False, data))
  584. elif not is_dynamic:
  585. opl.append(
  586. (False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+"))
  587. )
  588. else:
  589. opl.append((True, data))
  590. def _convert(elem: str) -> ast.stmt:
  591. ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
  592. ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2
  593. return ret
  594. def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]:
  595. parts = [
  596. _convert(elem) if is_dynamic else ast.Str(s=elem)
  597. for is_dynamic, elem in ops
  598. ]
  599. parts = parts or [ast.Str("")]
  600. # constant fold
  601. ret = [parts[0]]
  602. for p in parts[1:]:
  603. if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
  604. ret[-1] = ast.Str(ret[-1].s + p.s)
  605. else:
  606. ret.append(p)
  607. return ret
  608. dom_parts = _parts(dom_ops)
  609. url_parts = _parts(url_ops)
  610. if not append_unknown:
  611. body = []
  612. else:
  613. body = [_IF_KWARGS_URL_ENCODE_AST]
  614. url_parts.extend(_URL_ENCODE_AST_NAMES)
  615. def _join(parts: t.List[ast.AST]) -> ast.AST:
  616. if len(parts) == 1: # shortcut
  617. return parts[0]
  618. return ast.JoinedStr(parts)
  619. body.append(
  620. ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
  621. )
  622. pargs = [
  623. elem
  624. for is_dynamic, elem in dom_ops + url_ops
  625. if is_dynamic and elem not in defaults
  626. ]
  627. kargs = [str(k) for k in defaults]
  628. func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore
  629. func_ast.name = f"<builder:{self.rule!r}>"
  630. func_ast.args.args.append(ast.arg(".self", None))
  631. for arg in pargs + kargs:
  632. func_ast.args.args.append(ast.arg(arg, None))
  633. func_ast.args.kwarg = ast.arg(".kwargs", None)
  634. for _ in kargs:
  635. func_ast.args.defaults.append(ast.Str(""))
  636. func_ast.body = body
  637. # use `ast.parse` instead of `ast.Module` for better portability
  638. # Python 3.8 changes the signature of `ast.Module`
  639. module = ast.parse("")
  640. module.body = [func_ast]
  641. # mark everything as on line 1, offset 0
  642. # less error-prone than `ast.fix_missing_locations`
  643. # bad line numbers cause an assert to fail in debug builds
  644. for node in ast.walk(module):
  645. if "lineno" in node._attributes:
  646. node.lineno = 1
  647. if "end_lineno" in node._attributes:
  648. node.end_lineno = node.lineno # type: ignore[attr-defined]
  649. if "col_offset" in node._attributes:
  650. node.col_offset = 0
  651. if "end_col_offset" in node._attributes:
  652. node.end_col_offset = node.col_offset # type: ignore[attr-defined]
  653. code = compile(module, "<werkzeug routing>", "exec")
  654. return self._get_func_code(code, func_ast.name)
  655. def build(
  656. self, values: t.Mapping[str, t.Any], append_unknown: bool = True
  657. ) -> t.Optional[t.Tuple[str, str]]:
  658. """Assembles the relative url for that rule and the subdomain.
  659. If building doesn't work for some reasons `None` is returned.
  660. :internal:
  661. """
  662. try:
  663. if append_unknown:
  664. return self._build_unknown(**values)
  665. else:
  666. return self._build(**values)
  667. except ValidationError:
  668. return None
  669. def provides_defaults_for(self, rule: "Rule") -> bool:
  670. """Check if this rule has defaults for a given rule.
  671. :internal:
  672. """
  673. return bool(
  674. not self.build_only
  675. and self.defaults
  676. and self.endpoint == rule.endpoint
  677. and self != rule
  678. and self.arguments == rule.arguments
  679. )
  680. def suitable_for(
  681. self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None
  682. ) -> bool:
  683. """Check if the dict of values has enough data for url generation.
  684. :internal:
  685. """
  686. # if a method was given explicitly and that method is not supported
  687. # by this rule, this rule is not suitable.
  688. if (
  689. method is not None
  690. and self.methods is not None
  691. and method not in self.methods
  692. ):
  693. return False
  694. defaults = self.defaults or ()
  695. # all arguments required must be either in the defaults dict or
  696. # the value dictionary otherwise it's not suitable
  697. for key in self.arguments:
  698. if key not in defaults and key not in values:
  699. return False
  700. # in case defaults are given we ensure that either the value was
  701. # skipped or the value is the same as the default value.
  702. if defaults:
  703. for key, value in defaults.items():
  704. if key in values and value != values[key]:
  705. return False
  706. return True
  707. def build_compare_key(self) -> t.Tuple[int, int, int]:
  708. """The build compare key for sorting.
  709. :internal:
  710. """
  711. return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
  712. def __eq__(self, other: object) -> bool:
  713. return isinstance(other, type(self)) and self._trace == other._trace
  714. __hash__ = None # type: ignore
  715. def __str__(self) -> str:
  716. return self.rule
  717. def __repr__(self) -> str:
  718. if self.map is None:
  719. return f"<{type(self).__name__} (unbound)>"
  720. parts = []
  721. for is_dynamic, data in self._trace:
  722. if is_dynamic:
  723. parts.append(f"<{data}>")
  724. else:
  725. parts.append(data)
  726. parts = "".join(parts).lstrip("|")
  727. methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
  728. return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>"