1import ast
2import re
3import typing as t
4from dataclasses import dataclass
5from string import Template
6from types import CodeType
7
8from .._internal import _to_bytes
9from ..urls import url_encode
10from ..urls import url_quote
11from .converters import ValidationError
12
13if t.TYPE_CHECKING:
14 from .converters import BaseConverter
15 from .map import Map
16
17
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]
23
24
25@dataclass
26class RulePart:
27 """A part of a rule.
28
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.
33
34 """
35
36 content: str
37 final: bool
38 static: bool
39 suffixed: bool
40 weight: Weighting
41
42
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)
64
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)
80
81
82_PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
83
84
85def _find(value: str, target: str, pos: int) -> int:
86 """Find the *target* in *value* after *pos*.
87
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)
94
95
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)
107
108
109def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]:
110 argstr += ","
111 args = []
112 kwargs = {}
113
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
124
125 return tuple(args), kwargs
126
127
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 """
133
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()
138
139
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::
144
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 ])
153
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 """
158
159 def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
160 self.subdomain = subdomain
161 self.rules = rules
162
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
169
170
171class Submount(RuleFactory):
172 """Like `Subdomain` but prefixes the URL rule with a given string::
173
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 ])
181
182 Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
183 """
184
185 def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
186 self.path = path.rstrip("/")
187 self.rules = rules
188
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
195
196
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::
200
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 """
209
210 def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
211 self.prefix = prefix
212 self.rules = rules
213
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
220
221
222class RuleTemplate:
223 """Returns copies of the rules wrapped and expands string templates in
224 the endpoint, rule, defaults or subdomain sections.
225
226 Here a small example for such a rule template::
227
228 from werkzeug.routing import Map, Rule, RuleTemplate
229
230 resource = RuleTemplate([
231 Rule('/$name/', endpoint='$name.list'),
232 Rule('/$name/<int:id>', endpoint='$name.show')
233 ])
234
235 url_map = Map([resource(name='user'), resource(name='page')])
236
237 When a rule template is called the keyword arguments are used to
238 replace the placeholders in all the string parameters.
239 """
240
241 def __init__(self, rules: t.Iterable["Rule"]) -> None:
242 self.rules = list(rules)
243
244 def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory":
245 return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
246
247
248class RuleTemplateFactory(RuleFactory):
249 """A factory that fills in template variables into rules. Used by
250 `RuleTemplate` internally.
251
252 :internal:
253 """
254
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
260
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 )
285
286
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
296
297
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"))
308
309
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.
315
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.
321
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.
326
327 The converters are defined on the `Map`.
328
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.
333
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::
337
338 url_map = Map([
339 Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
340 Rule('/all/page/<int:page>', endpoint='all_entries')
341 ])
342
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.
347
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.
352
353 Can be useful if you want to have user profiles on different subdomains
354 and all subdomains are forwarded to your application::
355
356 url_map = Map([
357 Rule('/', subdomain='<username>', endpoint='user/homepage'),
358 Rule('/stats', subdomain='<username>', endpoint='user/stats')
359 ])
360
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.
369
370 `strict_slashes`
371 Override the `Map` setting for `strict_slashes` only for this rule. If
372 not specified the `Map` setting is used.
373
374 `merge_slashes`
375 Override :attr:`Map.merge_slashes` for this rule.
376
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)
381
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::
388
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)}'
393
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 ])
399
400 When the rule is matched the routing system will raise a
401 `RequestRedirect` exception with the target for the redirect.
402
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.
406
407 `alias`
408 If enabled this rule serves as an alias for another rule with the same
409 endpoint and arguments.
410
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.
415
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.
420
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.
425
426 .. versionadded:: 1.0
427 Added ``websocket``.
428
429 .. versionadded:: 1.0
430 Added ``merge_slashes``.
431
432 .. versionadded:: 0.7
433 Added ``alias`` and ``host``.
434
435 .. versionchanged:: 0.6.1
436 ``HEAD`` is added to ``methods`` if ``GET`` is present.
437 """
438
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("/")
459
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
469
470 if methods is not None:
471 if isinstance(methods, str):
472 raise TypeError("'methods' should be a list of strings.")
473
474 methods = {x.upper() for x in methods}
475
476 if "HEAD" not in methods and "GET" in methods:
477 methods.add("HEAD")
478
479 if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
480 raise ValueError(
481 "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
482 )
483
484 self.methods = methods
485 self.endpoint: str = endpoint # type: ignore
486 self.redirect_to = redirect_to
487
488 if defaults:
489 self.arguments = set(map(str, defaults))
490 else:
491 self.arguments = set()
492
493 self._converters: t.Dict[str, "BaseConverter"] = {}
494 self._trace: t.List[t.Tuple[bool, str]] = []
495 self._parts: t.List[RulePart] = []
496
497 def empty(self) -> "Rule":
498 """
499 Return an unbound copy of this rule.
500
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())
506
507 def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
508 """
509 Provides kwargs for instantiating empty copy with empty()
510
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.
514
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 )
533
534 def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
535 yield self
536
537 def refresh(self) -> None:
538 """Rebinds and refreshes the URL. Call this if you modified the
539 rule in place.
540
541 :internal:
542 """
543 self.bind(self.map, rebind=True)
544
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.
548
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()
561
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.
570
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)
576
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 )
584
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
591
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}")
597
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"])
603
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"]))
620
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
646
647 pos = match.end()
648
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 )
674
675 def compile(self) -> None:
676 """Compiles the regular expression and stores it."""
677 assert self.map is not None, "rule not bound"
678
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))
703
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)
708
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
715
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]] = []
722
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))
740
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
745
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
760
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)
768
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)
773
774 body.append(
775 ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
776 )
777
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]
784
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
794
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]
799
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]
812
813 code = compile(module, "<werkzeug routing>", "exec")
814 return self._get_func_code(code, func_ast.name)
815
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.
821
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
831
832 def provides_defaults_for(self, rule: "Rule") -> bool:
833 """Check if this rule has defaults for a given rule.
834
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 )
844
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.
849
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
860
861 defaults = self.defaults or ()
862
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
868
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
875
876 return True
877
878 def build_compare_key(self) -> t.Tuple[int, int, int]:
879 """The build compare key for sorting.
880
881 :internal:
882 """
883 return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
884
885 def __eq__(self, other: object) -> bool:
886 return isinstance(other, type(self)) and self._trace == other._trace
887
888 __hash__ = None # type: ignore
889
890 def __str__(self) -> str:
891 return self.rule
892
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}>"