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