Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/routing/rules.py: 69%
378 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +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("urls must start with a leading slash")
458 self.rule = string
459 self.is_leaf = not string.endswith("/")
460 self.is_branch = string.endswith("/")
462 self.map: Map = None # type: ignore
463 self.strict_slashes = strict_slashes
464 self.merge_slashes = merge_slashes
465 self.subdomain = subdomain
466 self.host = host
467 self.defaults = defaults
468 self.build_only = build_only
469 self.alias = alias
470 self.websocket = websocket
472 if methods is not None:
473 if isinstance(methods, str):
474 raise TypeError("'methods' should be a list of strings.")
476 methods = {x.upper() for x in methods}
478 if "HEAD" not in methods and "GET" in methods:
479 methods.add("HEAD")
481 if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
482 raise ValueError(
483 "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
484 )
486 self.methods = methods
487 self.endpoint: str = endpoint # type: ignore
488 self.redirect_to = redirect_to
490 if defaults:
491 self.arguments = set(map(str, defaults))
492 else:
493 self.arguments = set()
495 self._converters: dict[str, BaseConverter] = {}
496 self._trace: list[tuple[bool, str]] = []
497 self._parts: list[RulePart] = []
499 def empty(self) -> Rule:
500 """
501 Return an unbound copy of this rule.
503 This can be useful if want to reuse an already bound URL for another
504 map. See ``get_empty_kwargs`` to override what keyword arguments are
505 provided to the new copy.
506 """
507 return type(self)(self.rule, **self.get_empty_kwargs())
509 def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
510 """
511 Provides kwargs for instantiating empty copy with empty()
513 Use this method to provide custom keyword arguments to the subclass of
514 ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
515 has custom keyword arguments that are needed at instantiation.
517 Must return a ``dict`` that will be provided as kwargs to the new
518 instance of ``Rule``, following the initial ``self.rule`` value which
519 is always provided as the first, required positional argument.
520 """
521 defaults = None
522 if self.defaults:
523 defaults = dict(self.defaults)
524 return dict(
525 defaults=defaults,
526 subdomain=self.subdomain,
527 methods=self.methods,
528 build_only=self.build_only,
529 endpoint=self.endpoint,
530 strict_slashes=self.strict_slashes,
531 redirect_to=self.redirect_to,
532 alias=self.alias,
533 host=self.host,
534 )
536 def get_rules(self, map: Map) -> t.Iterator[Rule]:
537 yield self
539 def refresh(self) -> None:
540 """Rebinds and refreshes the URL. Call this if you modified the
541 rule in place.
543 :internal:
544 """
545 self.bind(self.map, rebind=True)
547 def bind(self, map: Map, rebind: bool = False) -> None:
548 """Bind the url to a map and create a regular expression based on
549 the information from the rule itself and the defaults from the map.
551 :internal:
552 """
553 if self.map is not None and not rebind:
554 raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
555 self.map = map
556 if self.strict_slashes is None:
557 self.strict_slashes = map.strict_slashes
558 if self.merge_slashes is None:
559 self.merge_slashes = map.merge_slashes
560 if self.subdomain is None:
561 self.subdomain = map.default_subdomain
562 self.compile()
564 def get_converter(
565 self,
566 variable_name: str,
567 converter_name: str,
568 args: t.Tuple,
569 kwargs: t.Mapping[str, t.Any],
570 ) -> BaseConverter:
571 """Looks up the converter for the given parameter.
573 .. versionadded:: 0.9
574 """
575 if converter_name not in self.map.converters:
576 raise LookupError(f"the converter {converter_name!r} does not exist")
577 return self.map.converters[converter_name](self.map, *args, **kwargs)
579 def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
580 items: t.Iterable[tuple[str, str]] = iter_multi_items(query_vars)
582 if self.map.sort_parameters:
583 items = sorted(items, key=self.map.sort_key)
585 return _urlencode(items, encoding=self.map.charset)
587 def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
588 content = ""
589 static = True
590 argument_weights = []
591 static_weights: list[tuple[int, int]] = []
592 final = False
593 convertor_number = 0
595 pos = 0
596 while pos < len(rule):
597 match = _part_re.match(rule, pos)
598 if match is None:
599 raise ValueError(f"malformed url rule: {rule!r}")
601 data = match.groupdict()
602 if data["static"] is not None:
603 static_weights.append((len(static_weights), -len(data["static"])))
604 self._trace.append((False, data["static"]))
605 content += data["static"] if static else re.escape(data["static"])
607 if data["variable"] is not None:
608 if static:
609 # Switching content to represent regex, hence the need to escape
610 content = re.escape(content)
611 static = False
612 c_args, c_kwargs = parse_converter_args(data["arguments"] or "")
613 convobj = self.get_converter(
614 data["variable"], data["converter"] or "default", c_args, c_kwargs
615 )
616 self._converters[data["variable"]] = convobj
617 self.arguments.add(data["variable"])
618 if not convobj.part_isolating:
619 final = True
620 content += f"(?P<__werkzeug_{convertor_number}>{convobj.regex})"
621 convertor_number += 1
622 argument_weights.append(convobj.weight)
623 self._trace.append((True, data["variable"]))
625 if data["slash"] is not None:
626 self._trace.append((False, "/"))
627 if final:
628 content += "/"
629 else:
630 if not static:
631 content += r"\Z"
632 weight = Weighting(
633 -len(static_weights),
634 static_weights,
635 -len(argument_weights),
636 argument_weights,
637 )
638 yield RulePart(
639 content=content,
640 final=final,
641 static=static,
642 suffixed=False,
643 weight=weight,
644 )
645 content = ""
646 static = True
647 argument_weights = []
648 static_weights = []
649 final = False
650 convertor_number = 0
652 pos = match.end()
654 suffixed = False
655 if final and content[-1] == "/":
656 # If a converter is part_isolating=False (matches slashes) and ends with a
657 # slash, augment the regex to support slash redirects.
658 suffixed = True
659 content = content[:-1] + "(?<!/)(/?)"
660 if not static:
661 content += r"\Z"
662 weight = Weighting(
663 -len(static_weights),
664 static_weights,
665 -len(argument_weights),
666 argument_weights,
667 )
668 yield RulePart(
669 content=content,
670 final=final,
671 static=static,
672 suffixed=suffixed,
673 weight=weight,
674 )
675 if suffixed:
676 yield RulePart(
677 content="", final=False, static=True, suffixed=False, weight=weight
678 )
680 def compile(self) -> None:
681 """Compiles the regular expression and stores it."""
682 assert self.map is not None, "rule not bound"
684 if self.map.host_matching:
685 domain_rule = self.host or ""
686 else:
687 domain_rule = self.subdomain or ""
688 self._parts = []
689 self._trace = []
690 self._converters = {}
691 if domain_rule == "":
692 self._parts = [
693 RulePart(
694 content="",
695 final=False,
696 static=True,
697 suffixed=False,
698 weight=Weighting(0, [], 0, []),
699 )
700 ]
701 else:
702 self._parts.extend(self._parse_rule(domain_rule))
703 self._trace.append((False, "|"))
704 rule = self.rule
705 if self.merge_slashes:
706 rule = re.sub("/{2,}?", "/", self.rule)
707 self._parts.extend(self._parse_rule(rule))
709 self._build: t.Callable[..., tuple[str, str]]
710 self._build = self._compile_builder(False).__get__(self, None)
711 self._build_unknown: t.Callable[..., tuple[str, str]]
712 self._build_unknown = self._compile_builder(True).__get__(self, None)
714 @staticmethod
715 def _get_func_code(code: CodeType, name: str) -> t.Callable[..., tuple[str, str]]:
716 globs: dict[str, t.Any] = {}
717 locs: dict[str, t.Any] = {}
718 exec(code, globs, locs)
719 return locs[name] # type: ignore
721 def _compile_builder(
722 self, append_unknown: bool = True
723 ) -> t.Callable[..., tuple[str, str]]:
724 defaults = self.defaults or {}
725 dom_ops: list[tuple[bool, str]] = []
726 url_ops: list[tuple[bool, str]] = []
728 opl = dom_ops
729 for is_dynamic, data in self._trace:
730 if data == "|" and opl is dom_ops:
731 opl = url_ops
732 continue
733 # this seems like a silly case to ever come up but:
734 # if a default is given for a value that appears in the rule,
735 # resolve it to a constant ahead of time
736 if is_dynamic and data in defaults:
737 data = self._converters[data].to_url(defaults[data])
738 opl.append((False, data))
739 elif not is_dynamic:
740 # safe = https://url.spec.whatwg.org/#url-path-segment-string
741 opl.append(
742 (
743 False,
744 quote(data, safe="!$&'()*+,/:;=@", encoding=self.map.charset),
745 )
746 )
747 else:
748 opl.append((True, data))
750 def _convert(elem: str) -> ast.stmt:
751 ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
752 ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2
753 return ret
755 def _parts(ops: list[tuple[bool, str]]) -> list[ast.AST]:
756 parts = [
757 _convert(elem) if is_dynamic else ast.Constant(elem)
758 for is_dynamic, elem in ops
759 ]
760 parts = parts or [ast.Constant("")]
761 # constant fold
762 ret = [parts[0]]
763 for p in parts[1:]:
764 if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant):
765 ret[-1] = ast.Constant(ret[-1].value + p.value)
766 else:
767 ret.append(p)
768 return ret
770 dom_parts = _parts(dom_ops)
771 url_parts = _parts(url_ops)
772 if not append_unknown:
773 body = []
774 else:
775 body = [_IF_KWARGS_URL_ENCODE_AST]
776 url_parts.extend(_URL_ENCODE_AST_NAMES)
778 def _join(parts: list[ast.AST]) -> ast.AST:
779 if len(parts) == 1: # shortcut
780 return parts[0]
781 return ast.JoinedStr(parts)
783 body.append(
784 ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
785 )
787 pargs = [
788 elem
789 for is_dynamic, elem in dom_ops + url_ops
790 if is_dynamic and elem not in defaults
791 ]
792 kargs = [str(k) for k in defaults]
794 func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore
795 func_ast.name = f"<builder:{self.rule!r}>"
796 func_ast.args.args.append(ast.arg(".self", None))
797 for arg in pargs + kargs:
798 func_ast.args.args.append(ast.arg(arg, None))
799 func_ast.args.kwarg = ast.arg(".kwargs", None)
800 for _ in kargs:
801 func_ast.args.defaults.append(ast.Constant(""))
802 func_ast.body = body
804 # Use `ast.parse` instead of `ast.Module` for better portability, since the
805 # signature of `ast.Module` can change.
806 module = ast.parse("")
807 module.body = [func_ast]
809 # mark everything as on line 1, offset 0
810 # less error-prone than `ast.fix_missing_locations`
811 # bad line numbers cause an assert to fail in debug builds
812 for node in ast.walk(module):
813 if "lineno" in node._attributes:
814 node.lineno = 1
815 if "end_lineno" in node._attributes:
816 node.end_lineno = node.lineno
817 if "col_offset" in node._attributes:
818 node.col_offset = 0
819 if "end_col_offset" in node._attributes:
820 node.end_col_offset = node.col_offset
822 code = compile(module, "<werkzeug routing>", "exec")
823 return self._get_func_code(code, func_ast.name)
825 def build(
826 self, values: t.Mapping[str, t.Any], append_unknown: bool = True
827 ) -> tuple[str, str] | None:
828 """Assembles the relative url for that rule and the subdomain.
829 If building doesn't work for some reasons `None` is returned.
831 :internal:
832 """
833 try:
834 if append_unknown:
835 return self._build_unknown(**values)
836 else:
837 return self._build(**values)
838 except ValidationError:
839 return None
841 def provides_defaults_for(self, rule: Rule) -> bool:
842 """Check if this rule has defaults for a given rule.
844 :internal:
845 """
846 return bool(
847 not self.build_only
848 and self.defaults
849 and self.endpoint == rule.endpoint
850 and self != rule
851 and self.arguments == rule.arguments
852 )
854 def suitable_for(
855 self, values: t.Mapping[str, t.Any], method: str | None = None
856 ) -> bool:
857 """Check if the dict of values has enough data for url generation.
859 :internal:
860 """
861 # if a method was given explicitly and that method is not supported
862 # by this rule, this rule is not suitable.
863 if (
864 method is not None
865 and self.methods is not None
866 and method not in self.methods
867 ):
868 return False
870 defaults = self.defaults or ()
872 # all arguments required must be either in the defaults dict or
873 # the value dictionary otherwise it's not suitable
874 for key in self.arguments:
875 if key not in defaults and key not in values:
876 return False
878 # in case defaults are given we ensure that either the value was
879 # skipped or the value is the same as the default value.
880 if defaults:
881 for key, value in defaults.items():
882 if key in values and value != values[key]:
883 return False
885 return True
887 def build_compare_key(self) -> tuple[int, int, int]:
888 """The build compare key for sorting.
890 :internal:
891 """
892 return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
894 def __eq__(self, other: object) -> bool:
895 return isinstance(other, type(self)) and self._trace == other._trace
897 __hash__ = None # type: ignore
899 def __str__(self) -> str:
900 return self.rule
902 def __repr__(self) -> str:
903 if self.map is None:
904 return f"<{type(self).__name__} (unbound)>"
905 parts = []
906 for is_dynamic, data in self._trace:
907 if is_dynamic:
908 parts.append(f"<{data}>")
909 else:
910 parts.append(data)
911 parts = "".join(parts).lstrip("|")
912 methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
913 return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>"