]> jfr.im git - dlqueue.git/blob - venv/lib/python3.11/site-packages/werkzeug/routing/rules.py
init: venv aand flask
[dlqueue.git] / venv / lib / python3.11 / site-packages / werkzeug / routing / rules.py
1 from __future__ import annotations
2
3 import ast
4 import re
5 import typing as t
6 from dataclasses import dataclass
7 from string import Template
8 from types import CodeType
9 from urllib.parse import quote
10
11 from ..datastructures import iter_multi_items
12 from ..urls import _urlencode
13 from .converters import ValidationError
14
15 if t.TYPE_CHECKING:
16 from .converters import BaseConverter
17 from .map import Map
18
19
20 class 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
28 class 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 ((?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 )
82
83
84 _PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
85
86
87 def _find(value: str, target: str, pos: int) -> int:
88 """Find the *target* in *value* after *pos*.
89
90 Returns the *value* length if *target* isn't found.
91 """
92 try:
93 return value.index(target, pos)
94 except ValueError:
95 return len(value)
96
97
98 def _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)
109
110
111 def parse_converter_args(argstr: str) -> tuple[t.Tuple, dict[str, t.Any]]:
112 argstr += ","
113 args = []
114 kwargs = {}
115
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
126
127 return tuple(args), kwargs
128
129
130 class 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 """
135
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()
140
141
142 class 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::
146
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 ])
155
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 """
160
161 def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
162 self.subdomain = subdomain
163 self.rules = rules
164
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
171
172
173 class Submount(RuleFactory):
174 """Like `Subdomain` but prefixes the URL rule with a given string::
175
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 ])
183
184 Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
185 """
186
187 def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
188 self.path = path.rstrip("/")
189 self.rules = rules
190
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
197
198
199 class EndpointPrefix(RuleFactory):
200 """Prefixes all endpoints (which must be strings for this factory) with
201 another string. This can be useful for sub applications::
202
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 """
211
212 def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
213 self.prefix = prefix
214 self.rules = rules
215
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
222
223
224 class RuleTemplate:
225 """Returns copies of the rules wrapped and expands string templates in
226 the endpoint, rule, defaults or subdomain sections.
227
228 Here a small example for such a rule template::
229
230 from werkzeug.routing import Map, Rule, RuleTemplate
231
232 resource = RuleTemplate([
233 Rule('/$name/', endpoint='$name.list'),
234 Rule('/$name/<int:id>', endpoint='$name.show')
235 ])
236
237 url_map = Map([resource(name='user'), resource(name='page')])
238
239 When a rule template is called the keyword arguments are used to
240 replace the placeholders in all the string parameters.
241 """
242
243 def __init__(self, rules: t.Iterable[Rule]) -> None:
244 self.rules = list(rules)
245
246 def __call__(self, *args: t.Any, **kwargs: t.Any) -> RuleTemplateFactory:
247 return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
248
249
250 class RuleTemplateFactory(RuleFactory):
251 """A factory that fills in template variables into rules. Used by
252 `RuleTemplate` internally.
253
254 :internal:
255 """
256
257 def __init__(
258 self, rules: t.Iterable[RuleFactory], context: dict[str, t.Any]
259 ) -> None:
260 self.rules = rules
261 self.context = context
262
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 )
287
288
289 def _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
298
299
300 _CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
301 _IF_KWARGS_URL_ENCODE_CODE = """\
302 if kwargs:
303 params = self._encode_query_vars(kwargs)
304 q = "?" if params else ""
305 else:
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"))
310
311
312 class 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.
317
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.
323
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.
328
329 The converters are defined on the `Map`.
330
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.
335
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::
339
340 url_map = Map([
341 Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
342 Rule('/all/page/<int:page>', endpoint='all_entries')
343 ])
344
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.
349
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.
354
355 Can be useful if you want to have user profiles on different subdomains
356 and all subdomains are forwarded to your application::
357
358 url_map = Map([
359 Rule('/', subdomain='<username>', endpoint='user/homepage'),
360 Rule('/stats', subdomain='<username>', endpoint='user/stats')
361 ])
362
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.
371
372 `strict_slashes`
373 Override the `Map` setting for `strict_slashes` only for this rule. If
374 not specified the `Map` setting is used.
375
376 `merge_slashes`
377 Override :attr:`Map.merge_slashes` for this rule.
378
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)
383
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::
390
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)}'
395
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 ])
401
402 When the rule is matched the routing system will raise a
403 `RequestRedirect` exception with the target for the redirect.
404
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.
408
409 `alias`
410 If enabled this rule serves as an alias for another rule with the same
411 endpoint and arguments.
412
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.
417
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.
422
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.
427
428 .. versionadded:: 1.0
429 Added ``websocket``.
430
431 .. versionadded:: 1.0
432 Added ``merge_slashes``.
433
434 .. versionadded:: 0.7
435 Added ``alias`` and ``host``.
436
437 .. versionchanged:: 0.6.1
438 ``HEAD`` is added to ``methods`` if ``GET`` is present.
439 """
440
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.")
458
459 self.rule = string
460 self.is_leaf = not string.endswith("/")
461 self.is_branch = string.endswith("/")
462
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
472
473 if methods is not None:
474 if isinstance(methods, str):
475 raise TypeError("'methods' should be a list of strings.")
476
477 methods = {x.upper() for x in methods}
478
479 if "HEAD" not in methods and "GET" in methods:
480 methods.add("HEAD")
481
482 if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
483 raise ValueError(
484 "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
485 )
486
487 self.methods = methods
488 self.endpoint: str = endpoint # type: ignore
489 self.redirect_to = redirect_to
490
491 if defaults:
492 self.arguments = set(map(str, defaults))
493 else:
494 self.arguments = set()
495
496 self._converters: dict[str, BaseConverter] = {}
497 self._trace: list[tuple[bool, str]] = []
498 self._parts: list[RulePart] = []
499
500 def empty(self) -> Rule:
501 """
502 Return an unbound copy of this rule.
503
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())
509
510 def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
511 """
512 Provides kwargs for instantiating empty copy with empty()
513
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.
517
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 )
536
537 def get_rules(self, map: Map) -> t.Iterator[Rule]:
538 yield self
539
540 def refresh(self) -> None:
541 """Rebinds and refreshes the URL. Call this if you modified the
542 rule in place.
543
544 :internal:
545 """
546 self.bind(self.map, rebind=True)
547
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.
551
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()
564
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.
573
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)
579
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)
582
583 if self.map.sort_parameters:
584 items = sorted(items, key=self.map.sort_key)
585
586 return _urlencode(items)
587
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
595
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}")
601
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"])
607
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"]))
625
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
652
653 pos = match.end()
654
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 )
680
681 def compile(self) -> None:
682 """Compiles the regular expression and stores it."""
683 assert self.map is not None, "rule not bound"
684
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))
709
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)
714
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
721
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]] = []
728
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))
745
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
750
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
765
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)
773
774 def _join(parts: list[ast.AST]) -> ast.AST:
775 if len(parts) == 1: # shortcut
776 return parts[0]
777 return ast.JoinedStr(parts)
778
779 body.append(
780 ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
781 )
782
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]
789
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
799
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]
804
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
817
818 code = compile(module, "<werkzeug routing>", "exec")
819 return self._get_func_code(code, func_ast.name)
820
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.
826
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
836
837 def provides_defaults_for(self, rule: Rule) -> bool:
838 """Check if this rule has defaults for a given rule.
839
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 )
849
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.
854
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
865
866 defaults = self.defaults or ()
867
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
873
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
880
881 return True
882
883 def build_compare_key(self) -> tuple[int, int, int]:
884 """The build compare key for sorting.
885
886 :internal:
887 """
888 return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
889
890 def __eq__(self, other: object) -> bool:
891 return isinstance(other, type(self)) and self._trace == other._trace
892
893 __hash__ = None # type: ignore
894
895 def __str__(self) -> str:
896 return self.rule
897
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}>"