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