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