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