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) # type: ignore
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
297def _prefix_names(src: str) -> ast.stmt:
298 """ast parse and prefix names with `.` to avoid collision with user vars"""
299 tree = ast.parse(src).body[0]
300 if isinstance(tree, ast.Expr):
301 tree = tree.value # type: ignore
302 for node in ast.walk(tree):
303 if isinstance(node, ast.Name):
304 node.id = f".{node.id}"
305 return tree
306
307
308_CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
309_IF_KWARGS_URL_ENCODE_CODE = """\
310if kwargs:
311 params = self._encode_query_vars(kwargs)
312 q = "?" if params else ""
313else:
314 q = params = ""
315"""
316_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE)
317_URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params"))
318
319
320class Rule(RuleFactory):
321 """A Rule represents one URL pattern. There are some options for `Rule`
322 that change the way it behaves and are passed to the `Rule` constructor.
323 Note that besides the rule-string all arguments *must* be keyword arguments
324 in order to not break the application on Werkzeug upgrades.
325
326 `string`
327 Rule strings basically are just normal URL paths with placeholders in
328 the format ``<converter(arguments):name>`` where the converter and the
329 arguments are optional. If no converter is defined the `default`
330 converter is used which means `string` in the normal configuration.
331
332 URL rules that end with a slash are branch URLs, others are leaves.
333 If you have `strict_slashes` enabled (which is the default), all
334 branch URLs that are matched without a trailing slash will trigger a
335 redirect to the same URL with the missing slash appended.
336
337 The converters are defined on the `Map`.
338
339 `endpoint`
340 The endpoint for this rule. This can be anything. A reference to a
341 function, a string, a number etc. The preferred way is using a string
342 because the endpoint is used for URL generation.
343
344 `defaults`
345 An optional dict with defaults for other rules with the same endpoint.
346 This is a bit tricky but useful if you want to have unique URLs::
347
348 url_map = Map([
349 Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
350 Rule('/all/page/<int:page>', endpoint='all_entries')
351 ])
352
353 If a user now visits ``http://example.com/all/page/1`` they will be
354 redirected to ``http://example.com/all/``. If `redirect_defaults` is
355 disabled on the `Map` instance this will only affect the URL
356 generation.
357
358 `subdomain`
359 The subdomain rule string for this rule. If not specified the rule
360 only matches for the `default_subdomain` of the map. If the map is
361 not bound to a subdomain this feature is disabled.
362
363 Can be useful if you want to have user profiles on different subdomains
364 and all subdomains are forwarded to your application::
365
366 url_map = Map([
367 Rule('/', subdomain='<username>', endpoint='user/homepage'),
368 Rule('/stats', subdomain='<username>', endpoint='user/stats')
369 ])
370
371 `methods`
372 A sequence of http methods this rule applies to. If not specified, all
373 methods are allowed. For example this can be useful if you want different
374 endpoints for `POST` and `GET`. If methods are defined and the path
375 matches but the method matched against is not in this list or in the
376 list of another rule for that path the error raised is of the type
377 `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the
378 list of methods and `HEAD` is not, `HEAD` is added automatically.
379
380 `strict_slashes`
381 Override the `Map` setting for `strict_slashes` only for this rule. If
382 not specified the `Map` setting is used.
383
384 `merge_slashes`
385 Override :attr:`Map.merge_slashes` for this rule.
386
387 `build_only`
388 Set this to True and the rule will never match but will create a URL
389 that can be build. This is useful if you have resources on a subdomain
390 or folder that are not handled by the WSGI application (like static data)
391
392 `redirect_to`
393 If given this must be either a string or callable. In case of a
394 callable it's called with the url adapter that triggered the match and
395 the values of the URL as keyword arguments and has to return the target
396 for the redirect, otherwise it has to be a string with placeholders in
397 rule syntax::
398
399 def foo_with_slug(adapter, id):
400 # ask the database for the slug for the old id. this of
401 # course has nothing to do with werkzeug.
402 return f'foo/{Foo.get_slug_for_id(id)}'
403
404 url_map = Map([
405 Rule('/foo/<slug>', endpoint='foo'),
406 Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
407 Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug)
408 ])
409
410 When the rule is matched the routing system will raise a
411 `RequestRedirect` exception with the target for the redirect.
412
413 Keep in mind that the URL will be joined against the URL root of the
414 script so don't use a leading slash on the target URL unless you
415 really mean root of that domain.
416
417 `alias`
418 If enabled this rule serves as an alias for another rule with the same
419 endpoint and arguments.
420
421 `host`
422 If provided and the URL map has host matching enabled this can be
423 used to provide a match rule for the whole host. This also means
424 that the subdomain feature is disabled.
425
426 `websocket`
427 If ``True``, this rule is only matches for WebSocket (``ws://``,
428 ``wss://``) requests. By default, rules will only match for HTTP
429 requests.
430
431 .. versionchanged:: 2.1
432 Percent-encoded newlines (``%0a``), which are decoded by WSGI
433 servers, are considered when routing instead of terminating the
434 match early.
435
436 .. versionadded:: 1.0
437 Added ``websocket``.
438
439 .. versionadded:: 1.0
440 Added ``merge_slashes``.
441
442 .. versionadded:: 0.7
443 Added ``alias`` and ``host``.
444
445 .. versionchanged:: 0.6.1
446 ``HEAD`` is added to ``methods`` if ``GET`` is present.
447 """
448
449 def __init__(
450 self,
451 string: str,
452 defaults: t.Mapping[str, t.Any] | None = None,
453 subdomain: str | None = None,
454 methods: t.Iterable[str] | None = None,
455 build_only: bool = False,
456 endpoint: t.Any | None = None,
457 strict_slashes: bool | None = None,
458 merge_slashes: bool | None = None,
459 redirect_to: str | t.Callable[..., str] | None = None,
460 alias: bool = False,
461 host: str | None = None,
462 websocket: bool = False,
463 ) -> None:
464 if not string.startswith("/"):
465 raise ValueError(f"URL rule '{string}' must start with a slash.")
466
467 self.rule = string
468 self.is_leaf = not string.endswith("/")
469 self.is_branch = string.endswith("/")
470
471 self.map: Map = None # type: ignore
472 self.strict_slashes = strict_slashes
473 self.merge_slashes = merge_slashes
474 self.subdomain = subdomain
475 self.host = host
476 self.defaults = defaults
477 self.build_only = build_only
478 self.alias = alias
479 self.websocket = websocket
480
481 if methods is not None:
482 if isinstance(methods, str):
483 raise TypeError("'methods' should be a list of strings.")
484
485 methods = {x.upper() for x in methods}
486
487 if "HEAD" not in methods and "GET" in methods:
488 methods.add("HEAD")
489
490 if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
491 raise ValueError(
492 "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
493 )
494
495 self.methods = methods
496 self.endpoint: t.Any = endpoint
497 self.redirect_to = redirect_to
498
499 if defaults:
500 self.arguments = set(map(str, defaults))
501 else:
502 self.arguments = set()
503
504 self._converters: dict[str, BaseConverter] = {}
505 self._trace: list[tuple[bool, str]] = []
506 self._parts: list[RulePart] = []
507
508 def empty(self) -> Rule:
509 """
510 Return an unbound copy of this rule.
511
512 This can be useful if want to reuse an already bound URL for another
513 map. See ``get_empty_kwargs`` to override what keyword arguments are
514 provided to the new copy.
515 """
516 return type(self)(self.rule, **self.get_empty_kwargs())
517
518 def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
519 """
520 Provides kwargs for instantiating empty copy with empty()
521
522 Use this method to provide custom keyword arguments to the subclass of
523 ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
524 has custom keyword arguments that are needed at instantiation.
525
526 Must return a ``dict`` that will be provided as kwargs to the new
527 instance of ``Rule``, following the initial ``self.rule`` value which
528 is always provided as the first, required positional argument.
529 """
530 defaults = None
531 if self.defaults:
532 defaults = dict(self.defaults)
533 return dict(
534 defaults=defaults,
535 subdomain=self.subdomain,
536 methods=self.methods,
537 build_only=self.build_only,
538 endpoint=self.endpoint,
539 strict_slashes=self.strict_slashes,
540 redirect_to=self.redirect_to,
541 alias=self.alias,
542 host=self.host,
543 )
544
545 def get_rules(self, map: Map) -> t.Iterator[Rule]:
546 yield self
547
548 def refresh(self) -> None:
549 """Rebinds and refreshes the URL. Call this if you modified the
550 rule in place.
551
552 :internal:
553 """
554 self.bind(self.map, rebind=True)
555
556 def bind(self, map: Map, rebind: bool = False) -> None:
557 """Bind the url to a map and create a regular expression based on
558 the information from the rule itself and the defaults from the map.
559
560 :internal:
561 """
562 if self.map is not None and not rebind:
563 raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
564 self.map = map
565 if self.strict_slashes is None:
566 self.strict_slashes = map.strict_slashes
567 if self.merge_slashes is None:
568 self.merge_slashes = map.merge_slashes
569 if self.subdomain is None:
570 self.subdomain = map.default_subdomain
571 self.compile()
572
573 def get_converter(
574 self,
575 variable_name: str,
576 converter_name: str,
577 args: tuple[t.Any, ...],
578 kwargs: t.Mapping[str, t.Any],
579 ) -> BaseConverter:
580 """Looks up the converter for the given parameter.
581
582 .. versionadded:: 0.9
583 """
584 if converter_name not in self.map.converters:
585 raise LookupError(f"the converter {converter_name!r} does not exist")
586 return self.map.converters[converter_name](self.map, *args, **kwargs)
587
588 def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
589 items: t.Iterable[tuple[str, str]] = iter_multi_items(query_vars)
590
591 if self.map.sort_parameters:
592 items = sorted(items, key=self.map.sort_key)
593
594 return _urlencode(items)
595
596 def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
597 content = ""
598 static = True
599 argument_weights = []
600 static_weights: list[tuple[int, int]] = []
601 final = False
602 convertor_number = 0
603
604 pos = 0
605 while pos < len(rule):
606 match = _part_re.match(rule, pos)
607 if match is None:
608 raise ValueError(f"malformed url rule: {rule!r}")
609
610 data = match.groupdict()
611 if data["static"] is not None:
612 static_weights.append((len(static_weights), -len(data["static"])))
613 self._trace.append((False, data["static"]))
614 content += data["static"] if static else re.escape(data["static"])
615
616 if data["variable"] is not None:
617 if static:
618 # Switching content to represent regex, hence the need to escape
619 content = re.escape(content)
620 static = False
621 c_args, c_kwargs = parse_converter_args(data["arguments"] or "")
622 convobj = self.get_converter(
623 data["variable"], data["converter"] or "default", c_args, c_kwargs
624 )
625 self._converters[data["variable"]] = convobj
626 self.arguments.add(data["variable"])
627 if not convobj.part_isolating:
628 final = True
629 content += f"(?P<__werkzeug_{convertor_number}>{convobj.regex})"
630 convertor_number += 1
631 argument_weights.append(convobj.weight)
632 self._trace.append((True, data["variable"]))
633
634 if data["slash"] is not None:
635 self._trace.append((False, "/"))
636 if final:
637 content += "/"
638 else:
639 if not static:
640 content += r"\Z"
641 weight = Weighting(
642 -len(static_weights),
643 static_weights,
644 -len(argument_weights),
645 argument_weights,
646 )
647 yield RulePart(
648 content=content,
649 final=final,
650 static=static,
651 suffixed=False,
652 weight=weight,
653 )
654 content = ""
655 static = True
656 argument_weights = []
657 static_weights = []
658 final = False
659 convertor_number = 0
660
661 pos = match.end()
662
663 suffixed = False
664 if final and content[-1] == "/":
665 # If a converter is part_isolating=False (matches slashes) and ends with a
666 # slash, augment the regex to support slash redirects.
667 suffixed = True
668 content = content[:-1] + "(?<!/)(/?)"
669 if not static:
670 content += r"\Z"
671 weight = Weighting(
672 -len(static_weights),
673 static_weights,
674 -len(argument_weights),
675 argument_weights,
676 )
677 yield RulePart(
678 content=content,
679 final=final,
680 static=static,
681 suffixed=suffixed,
682 weight=weight,
683 )
684 if suffixed:
685 yield RulePart(
686 content="", final=False, static=True, suffixed=False, weight=weight
687 )
688
689 def compile(self) -> None:
690 """Compiles the regular expression and stores it."""
691 assert self.map is not None, "rule not bound"
692
693 if self.map.host_matching:
694 domain_rule = self.host or ""
695 else:
696 domain_rule = self.subdomain or ""
697 self._parts = []
698 self._trace = []
699 self._converters = {}
700 if domain_rule == "":
701 self._parts = [
702 RulePart(
703 content="",
704 final=False,
705 static=True,
706 suffixed=False,
707 weight=Weighting(0, [], 0, []),
708 )
709 ]
710 else:
711 self._parts.extend(self._parse_rule(domain_rule))
712 self._trace.append((False, "|"))
713 rule = self.rule
714 if self.merge_slashes:
715 rule = re.sub("/{2,}?", "/", self.rule)
716 self._parts.extend(self._parse_rule(rule))
717
718 self._build: t.Callable[..., tuple[str, str]]
719 self._build = self._compile_builder(False).__get__(self, None)
720 self._build_unknown: t.Callable[..., tuple[str, str]]
721 self._build_unknown = self._compile_builder(True).__get__(self, None)
722
723 @staticmethod
724 def _get_func_code(code: CodeType, name: str) -> t.Callable[..., tuple[str, str]]:
725 globs: dict[str, t.Any] = {}
726 locs: dict[str, t.Any] = {}
727 exec(code, globs, locs)
728 return locs[name] # type: ignore
729
730 def _compile_builder(
731 self, append_unknown: bool = True
732 ) -> t.Callable[..., tuple[str, str]]:
733 defaults = self.defaults or {}
734 dom_ops: list[tuple[bool, str]] = []
735 url_ops: list[tuple[bool, str]] = []
736
737 opl = dom_ops
738 for is_dynamic, data in self._trace:
739 if data == "|" and opl is dom_ops:
740 opl = url_ops
741 continue
742 # this seems like a silly case to ever come up but:
743 # if a default is given for a value that appears in the rule,
744 # resolve it to a constant ahead of time
745 if is_dynamic and data in defaults:
746 data = self._converters[data].to_url(defaults[data])
747 opl.append((False, data))
748 elif not is_dynamic:
749 # safe = https://url.spec.whatwg.org/#url-path-segment-string
750 opl.append((False, quote(data, safe="!$&'()*+,/:;=@")))
751 else:
752 opl.append((True, data))
753
754 def _convert(elem: str) -> ast.stmt:
755 ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
756 ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2
757 return ret
758
759 def _parts(ops: list[tuple[bool, str]]) -> list[ast.AST]:
760 parts = [
761 _convert(elem) if is_dynamic else ast.Constant(elem)
762 for is_dynamic, elem in ops
763 ]
764 parts = parts or [ast.Constant("")]
765 # constant fold
766 ret = [parts[0]]
767 for p in parts[1:]:
768 if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant):
769 ret[-1] = ast.Constant(ret[-1].value + p.value)
770 else:
771 ret.append(p)
772 return ret
773
774 dom_parts = _parts(dom_ops)
775 url_parts = _parts(url_ops)
776 if not append_unknown:
777 body = []
778 else:
779 body = [_IF_KWARGS_URL_ENCODE_AST]
780 url_parts.extend(_URL_ENCODE_AST_NAMES)
781
782 def _join(parts: list[ast.AST]) -> ast.AST:
783 if len(parts) == 1: # shortcut
784 return parts[0]
785 return ast.JoinedStr(parts)
786
787 body.append(
788 ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
789 )
790
791 pargs = [
792 elem
793 for is_dynamic, elem in dom_ops + url_ops
794 if is_dynamic and elem not in defaults
795 ]
796 kargs = [str(k) for k in defaults]
797
798 func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore
799 func_ast.name = f"<builder:{self.rule!r}>"
800 func_ast.args.args.append(ast.arg(".self", None))
801 for arg in pargs + kargs:
802 func_ast.args.args.append(ast.arg(arg, None))
803 func_ast.args.kwarg = ast.arg(".kwargs", None)
804 for _ in kargs:
805 func_ast.args.defaults.append(ast.Constant(""))
806 func_ast.body = body
807
808 # Use `ast.parse` instead of `ast.Module` for better portability, since the
809 # signature of `ast.Module` can change.
810 module = ast.parse("")
811 module.body = [func_ast]
812
813 # mark everything as on line 1, offset 0
814 # less error-prone than `ast.fix_missing_locations`
815 # bad line numbers cause an assert to fail in debug builds
816 for node in ast.walk(module):
817 if "lineno" in node._attributes:
818 node.lineno = 1
819 if "end_lineno" in node._attributes:
820 node.end_lineno = node.lineno
821 if "col_offset" in node._attributes:
822 node.col_offset = 0
823 if "end_col_offset" in node._attributes:
824 node.end_col_offset = node.col_offset
825
826 code = compile(module, "<werkzeug routing>", "exec")
827 return self._get_func_code(code, func_ast.name)
828
829 def build(
830 self, values: t.Mapping[str, t.Any], append_unknown: bool = True
831 ) -> tuple[str, str] | None:
832 """Assembles the relative url for that rule and the subdomain.
833 If building doesn't work for some reasons `None` is returned.
834
835 :internal:
836 """
837 try:
838 if append_unknown:
839 return self._build_unknown(**values)
840 else:
841 return self._build(**values)
842 except ValidationError:
843 return None
844
845 def provides_defaults_for(self, rule: Rule) -> bool:
846 """Check if this rule has defaults for a given rule.
847
848 :internal:
849 """
850 return bool(
851 not self.build_only
852 and self.defaults
853 and self.endpoint == rule.endpoint
854 and self != rule
855 and self.arguments == rule.arguments
856 )
857
858 def suitable_for(
859 self, values: t.Mapping[str, t.Any], method: str | None = None
860 ) -> bool:
861 """Check if the dict of values has enough data for url generation.
862
863 :internal:
864 """
865 # if a method was given explicitly and that method is not supported
866 # by this rule, this rule is not suitable.
867 if (
868 method is not None
869 and self.methods is not None
870 and method not in self.methods
871 ):
872 return False
873
874 defaults = self.defaults or ()
875
876 # all arguments required must be either in the defaults dict or
877 # the value dictionary otherwise it's not suitable
878 for key in self.arguments:
879 if key not in defaults and key not in values:
880 return False
881
882 # in case defaults are given we ensure that either the value was
883 # skipped or the value is the same as the default value.
884 if defaults:
885 for key, value in defaults.items():
886 if key in values and value != values[key]:
887 return False
888
889 return True
890
891 def build_compare_key(self) -> tuple[int, int, int]:
892 """The build compare key for sorting.
893
894 :internal:
895 """
896 return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
897
898 def __eq__(self, other: object) -> bool:
899 return isinstance(other, type(self)) and self._trace == other._trace
900
901 __hash__ = None # type: ignore
902
903 def __str__(self) -> str:
904 return self.rule
905
906 def __repr__(self) -> str:
907 if self.map is None:
908 return f"<{type(self).__name__} (unbound)>"
909 parts = []
910 for is_dynamic, data in self._trace:
911 if is_dynamic:
912 parts.append(f"<{data}>")
913 else:
914 parts.append(data)
915 parts_str = "".join(parts).lstrip("|")
916 methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
917 return f"<{type(self).__name__} {parts_str!r}{methods} -> {self.endpoint}>"