123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879 |
- import ast
- import re
- import typing as t
- from dataclasses import dataclass
- from string import Template
- from types import CodeType
- from .._internal import _to_bytes
- from ..urls import url_encode
- from ..urls import url_quote
- from .converters import ValidationError
- if t.TYPE_CHECKING:
- from .converters import BaseConverter
- from .map import Map
- class Weighting(t.NamedTuple):
- number_static_weights: int
- static_weights: t.List[t.Tuple[int, int]]
- number_argument_weights: int
- argument_weights: t.List[int]
- @dataclass
- class RulePart:
- """A part of a rule.
- Rules can be represented by parts as delimited by `/` with
- instances of this class representing those parts. The *content* is
- either the raw content if *static* or a regex string to match
- against. The *weight* can be used to order parts when matching.
- """
- content: str
- final: bool
- static: bool
- weight: Weighting
- _part_re = re.compile(
- r"""
- (?:
- (?P<slash>\/) # a slash
- |
- (?P<static>[^<\/]+) # static rule data
- |
- (?:
- <
- (?:
- (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name
- (?:\((?P<arguments>.*?)\))? # converter arguments
- \: # variable delimiter
- )?
- (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name
- >
- )
- )
- """,
- re.VERBOSE,
- )
- _simple_rule_re = re.compile(r"<([^>]+)>")
- _converter_args_re = re.compile(
- r"""
- ((?P<name>\w+)\s*=\s*)?
- (?P<value>
- True|False|
- \d+.\d+|
- \d+.|
- \d+|
- [\w\d_.]+|
- [urUR]?(?P<stringval>"[^"]*?"|'[^']*')
- )\s*,
- """,
- re.VERBOSE,
- )
- _PYTHON_CONSTANTS = {"None": None, "True": True, "False": False}
- def _find(value: str, target: str, pos: int) -> int:
- """Find the *target* in *value* after *pos*.
- Returns the *value* length if *target* isn't found.
- """
- try:
- return value.index(target, pos)
- except ValueError:
- return len(value)
- def _pythonize(value: str) -> t.Union[None, bool, int, float, str]:
- if value in _PYTHON_CONSTANTS:
- return _PYTHON_CONSTANTS[value]
- for convert in int, float:
- try:
- return convert(value) # type: ignore
- except ValueError:
- pass
- if value[:1] == value[-1:] and value[0] in "\"'":
- value = value[1:-1]
- return str(value)
- def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]:
- argstr += ","
- args = []
- kwargs = {}
- for item in _converter_args_re.finditer(argstr):
- value = item.group("stringval")
- if value is None:
- value = item.group("value")
- value = _pythonize(value)
- if not item.group("name"):
- args.append(value)
- else:
- name = item.group("name")
- kwargs[name] = value
- return tuple(args), kwargs
- class RuleFactory:
- """As soon as you have more complex URL setups it's a good idea to use rule
- factories to avoid repetitive tasks. Some of them are builtin, others can
- be added by subclassing `RuleFactory` and overriding `get_rules`.
- """
- def get_rules(self, map: "Map") -> t.Iterable["Rule"]:
- """Subclasses of `RuleFactory` have to override this method and return
- an iterable of rules."""
- raise NotImplementedError()
- class Subdomain(RuleFactory):
- """All URLs provided by this factory have the subdomain set to a
- specific domain. For example if you want to use the subdomain for
- the current language this can be a good setup::
- url_map = Map([
- Rule('/', endpoint='#select_language'),
- Subdomain('<string(length=2):lang_code>', [
- Rule('/', endpoint='index'),
- Rule('/about', endpoint='about'),
- Rule('/help', endpoint='help')
- ])
- ])
- All the rules except for the ``'#select_language'`` endpoint will now
- listen on a two letter long subdomain that holds the language code
- for the current request.
- """
- def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None:
- self.subdomain = subdomain
- self.rules = rules
- def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
- for rulefactory in self.rules:
- for rule in rulefactory.get_rules(map):
- rule = rule.empty()
- rule.subdomain = self.subdomain
- yield rule
- class Submount(RuleFactory):
- """Like `Subdomain` but prefixes the URL rule with a given string::
- url_map = Map([
- Rule('/', endpoint='index'),
- Submount('/blog', [
- Rule('/', endpoint='blog/index'),
- Rule('/entry/<entry_slug>', endpoint='blog/show')
- ])
- ])
- Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``.
- """
- def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None:
- self.path = path.rstrip("/")
- self.rules = rules
- def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
- for rulefactory in self.rules:
- for rule in rulefactory.get_rules(map):
- rule = rule.empty()
- rule.rule = self.path + rule.rule
- yield rule
- class EndpointPrefix(RuleFactory):
- """Prefixes all endpoints (which must be strings for this factory) with
- another string. This can be useful for sub applications::
- url_map = Map([
- Rule('/', endpoint='index'),
- EndpointPrefix('blog/', [Submount('/blog', [
- Rule('/', endpoint='index'),
- Rule('/entry/<entry_slug>', endpoint='show')
- ])])
- ])
- """
- def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None:
- self.prefix = prefix
- self.rules = rules
- def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
- for rulefactory in self.rules:
- for rule in rulefactory.get_rules(map):
- rule = rule.empty()
- rule.endpoint = self.prefix + rule.endpoint
- yield rule
- class RuleTemplate:
- """Returns copies of the rules wrapped and expands string templates in
- the endpoint, rule, defaults or subdomain sections.
- Here a small example for such a rule template::
- from werkzeug.routing import Map, Rule, RuleTemplate
- resource = RuleTemplate([
- Rule('/$name/', endpoint='$name.list'),
- Rule('/$name/<int:id>', endpoint='$name.show')
- ])
- url_map = Map([resource(name='user'), resource(name='page')])
- When a rule template is called the keyword arguments are used to
- replace the placeholders in all the string parameters.
- """
- def __init__(self, rules: t.Iterable["Rule"]) -> None:
- self.rules = list(rules)
- def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory":
- return RuleTemplateFactory(self.rules, dict(*args, **kwargs))
- class RuleTemplateFactory(RuleFactory):
- """A factory that fills in template variables into rules. Used by
- `RuleTemplate` internally.
- :internal:
- """
- def __init__(
- self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any]
- ) -> None:
- self.rules = rules
- self.context = context
- def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
- for rulefactory in self.rules:
- for rule in rulefactory.get_rules(map):
- new_defaults = subdomain = None
- if rule.defaults:
- new_defaults = {}
- for key, value in rule.defaults.items():
- if isinstance(value, str):
- value = Template(value).substitute(self.context)
- new_defaults[key] = value
- if rule.subdomain is not None:
- subdomain = Template(rule.subdomain).substitute(self.context)
- new_endpoint = rule.endpoint
- if isinstance(new_endpoint, str):
- new_endpoint = Template(new_endpoint).substitute(self.context)
- yield Rule(
- Template(rule.rule).substitute(self.context),
- new_defaults,
- subdomain,
- rule.methods,
- rule.build_only,
- new_endpoint,
- rule.strict_slashes,
- )
- def _prefix_names(src: str) -> ast.stmt:
- """ast parse and prefix names with `.` to avoid collision with user vars"""
- tree = ast.parse(src).body[0]
- if isinstance(tree, ast.Expr):
- tree = tree.value # type: ignore
- for node in ast.walk(tree):
- if isinstance(node, ast.Name):
- node.id = f".{node.id}"
- return tree
- _CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()"
- _IF_KWARGS_URL_ENCODE_CODE = """\
- if kwargs:
- params = self._encode_query_vars(kwargs)
- q = "?" if params else ""
- else:
- q = params = ""
- """
- _IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE)
- _URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params"))
- class Rule(RuleFactory):
- """A Rule represents one URL pattern. There are some options for `Rule`
- that change the way it behaves and are passed to the `Rule` constructor.
- Note that besides the rule-string all arguments *must* be keyword arguments
- in order to not break the application on Werkzeug upgrades.
- `string`
- Rule strings basically are just normal URL paths with placeholders in
- the format ``<converter(arguments):name>`` where the converter and the
- arguments are optional. If no converter is defined the `default`
- converter is used which means `string` in the normal configuration.
- URL rules that end with a slash are branch URLs, others are leaves.
- If you have `strict_slashes` enabled (which is the default), all
- branch URLs that are matched without a trailing slash will trigger a
- redirect to the same URL with the missing slash appended.
- The converters are defined on the `Map`.
- `endpoint`
- The endpoint for this rule. This can be anything. A reference to a
- function, a string, a number etc. The preferred way is using a string
- because the endpoint is used for URL generation.
- `defaults`
- An optional dict with defaults for other rules with the same endpoint.
- This is a bit tricky but useful if you want to have unique URLs::
- url_map = Map([
- Rule('/all/', defaults={'page': 1}, endpoint='all_entries'),
- Rule('/all/page/<int:page>', endpoint='all_entries')
- ])
- If a user now visits ``http://example.com/all/page/1`` they will be
- redirected to ``http://example.com/all/``. If `redirect_defaults` is
- disabled on the `Map` instance this will only affect the URL
- generation.
- `subdomain`
- The subdomain rule string for this rule. If not specified the rule
- only matches for the `default_subdomain` of the map. If the map is
- not bound to a subdomain this feature is disabled.
- Can be useful if you want to have user profiles on different subdomains
- and all subdomains are forwarded to your application::
- url_map = Map([
- Rule('/', subdomain='<username>', endpoint='user/homepage'),
- Rule('/stats', subdomain='<username>', endpoint='user/stats')
- ])
- `methods`
- A sequence of http methods this rule applies to. If not specified, all
- methods are allowed. For example this can be useful if you want different
- endpoints for `POST` and `GET`. If methods are defined and the path
- matches but the method matched against is not in this list or in the
- list of another rule for that path the error raised is of the type
- `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the
- list of methods and `HEAD` is not, `HEAD` is added automatically.
- `strict_slashes`
- Override the `Map` setting for `strict_slashes` only for this rule. If
- not specified the `Map` setting is used.
- `merge_slashes`
- Override :attr:`Map.merge_slashes` for this rule.
- `build_only`
- Set this to True and the rule will never match but will create a URL
- that can be build. This is useful if you have resources on a subdomain
- or folder that are not handled by the WSGI application (like static data)
- `redirect_to`
- If given this must be either a string or callable. In case of a
- callable it's called with the url adapter that triggered the match and
- the values of the URL as keyword arguments and has to return the target
- for the redirect, otherwise it has to be a string with placeholders in
- rule syntax::
- def foo_with_slug(adapter, id):
- # ask the database for the slug for the old id. this of
- # course has nothing to do with werkzeug.
- return f'foo/{Foo.get_slug_for_id(id)}'
- url_map = Map([
- Rule('/foo/<slug>', endpoint='foo'),
- Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'),
- Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug)
- ])
- When the rule is matched the routing system will raise a
- `RequestRedirect` exception with the target for the redirect.
- Keep in mind that the URL will be joined against the URL root of the
- script so don't use a leading slash on the target URL unless you
- really mean root of that domain.
- `alias`
- If enabled this rule serves as an alias for another rule with the same
- endpoint and arguments.
- `host`
- If provided and the URL map has host matching enabled this can be
- used to provide a match rule for the whole host. This also means
- that the subdomain feature is disabled.
- `websocket`
- If ``True``, this rule is only matches for WebSocket (``ws://``,
- ``wss://``) requests. By default, rules will only match for HTTP
- requests.
- .. versionchanged:: 2.1
- Percent-encoded newlines (``%0a``), which are decoded by WSGI
- servers, are considered when routing instead of terminating the
- match early.
- .. versionadded:: 1.0
- Added ``websocket``.
- .. versionadded:: 1.0
- Added ``merge_slashes``.
- .. versionadded:: 0.7
- Added ``alias`` and ``host``.
- .. versionchanged:: 0.6.1
- ``HEAD`` is added to ``methods`` if ``GET`` is present.
- """
- def __init__(
- self,
- string: str,
- defaults: t.Optional[t.Mapping[str, t.Any]] = None,
- subdomain: t.Optional[str] = None,
- methods: t.Optional[t.Iterable[str]] = None,
- build_only: bool = False,
- endpoint: t.Optional[str] = None,
- strict_slashes: t.Optional[bool] = None,
- merge_slashes: t.Optional[bool] = None,
- redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None,
- alias: bool = False,
- host: t.Optional[str] = None,
- websocket: bool = False,
- ) -> None:
- if not string.startswith("/"):
- raise ValueError("urls must start with a leading slash")
- self.rule = string
- self.is_leaf = not string.endswith("/")
- self.is_branch = string.endswith("/")
- self.map: "Map" = None # type: ignore
- self.strict_slashes = strict_slashes
- self.merge_slashes = merge_slashes
- self.subdomain = subdomain
- self.host = host
- self.defaults = defaults
- self.build_only = build_only
- self.alias = alias
- self.websocket = websocket
- if methods is not None:
- if isinstance(methods, str):
- raise TypeError("'methods' should be a list of strings.")
- methods = {x.upper() for x in methods}
- if "HEAD" not in methods and "GET" in methods:
- methods.add("HEAD")
- if websocket and methods - {"GET", "HEAD", "OPTIONS"}:
- raise ValueError(
- "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods."
- )
- self.methods = methods
- self.endpoint: str = endpoint # type: ignore
- self.redirect_to = redirect_to
- if defaults:
- self.arguments = set(map(str, defaults))
- else:
- self.arguments = set()
- self._converters: t.Dict[str, "BaseConverter"] = {}
- self._trace: t.List[t.Tuple[bool, str]] = []
- self._parts: t.List[RulePart] = []
- def empty(self) -> "Rule":
- """
- Return an unbound copy of this rule.
- This can be useful if want to reuse an already bound URL for another
- map. See ``get_empty_kwargs`` to override what keyword arguments are
- provided to the new copy.
- """
- return type(self)(self.rule, **self.get_empty_kwargs())
- def get_empty_kwargs(self) -> t.Mapping[str, t.Any]:
- """
- Provides kwargs for instantiating empty copy with empty()
- Use this method to provide custom keyword arguments to the subclass of
- ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass
- has custom keyword arguments that are needed at instantiation.
- Must return a ``dict`` that will be provided as kwargs to the new
- instance of ``Rule``, following the initial ``self.rule`` value which
- is always provided as the first, required positional argument.
- """
- defaults = None
- if self.defaults:
- defaults = dict(self.defaults)
- return dict(
- defaults=defaults,
- subdomain=self.subdomain,
- methods=self.methods,
- build_only=self.build_only,
- endpoint=self.endpoint,
- strict_slashes=self.strict_slashes,
- redirect_to=self.redirect_to,
- alias=self.alias,
- host=self.host,
- )
- def get_rules(self, map: "Map") -> t.Iterator["Rule"]:
- yield self
- def refresh(self) -> None:
- """Rebinds and refreshes the URL. Call this if you modified the
- rule in place.
- :internal:
- """
- self.bind(self.map, rebind=True)
- def bind(self, map: "Map", rebind: bool = False) -> None:
- """Bind the url to a map and create a regular expression based on
- the information from the rule itself and the defaults from the map.
- :internal:
- """
- if self.map is not None and not rebind:
- raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}")
- self.map = map
- if self.strict_slashes is None:
- self.strict_slashes = map.strict_slashes
- if self.merge_slashes is None:
- self.merge_slashes = map.merge_slashes
- if self.subdomain is None:
- self.subdomain = map.default_subdomain
- self.compile()
- def get_converter(
- self,
- variable_name: str,
- converter_name: str,
- args: t.Tuple,
- kwargs: t.Mapping[str, t.Any],
- ) -> "BaseConverter":
- """Looks up the converter for the given parameter.
- .. versionadded:: 0.9
- """
- if converter_name not in self.map.converters:
- raise LookupError(f"the converter {converter_name!r} does not exist")
- return self.map.converters[converter_name](self.map, *args, **kwargs)
- def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str:
- return url_encode(
- query_vars,
- charset=self.map.charset,
- sort=self.map.sort_parameters,
- key=self.map.sort_key,
- )
- def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
- content = ""
- static = True
- argument_weights = []
- static_weights: t.List[t.Tuple[int, int]] = []
- final = False
- pos = 0
- while pos < len(rule):
- match = _part_re.match(rule, pos)
- if match is None:
- raise ValueError(f"malformed url rule: {rule!r}")
- data = match.groupdict()
- if data["static"] is not None:
- static_weights.append((len(static_weights), -len(data["static"])))
- self._trace.append((False, data["static"]))
- content += data["static"] if static else re.escape(data["static"])
- if data["variable"] is not None:
- if static:
- # Switching content to represent regex, hence the need to escape
- content = re.escape(content)
- static = False
- c_args, c_kwargs = parse_converter_args(data["arguments"] or "")
- convobj = self.get_converter(
- data["variable"], data["converter"] or "default", c_args, c_kwargs
- )
- self._converters[data["variable"]] = convobj
- self.arguments.add(data["variable"])
- if not convobj.part_isolating:
- final = True
- content += f"({convobj.regex})"
- argument_weights.append(convobj.weight)
- self._trace.append((True, data["variable"]))
- if data["slash"] is not None:
- self._trace.append((False, "/"))
- if final:
- content += "/"
- else:
- if not static:
- content += r"\Z"
- weight = Weighting(
- -len(static_weights),
- static_weights,
- -len(argument_weights),
- argument_weights,
- )
- yield RulePart(
- content=content, final=final, static=static, weight=weight
- )
- content = ""
- static = True
- argument_weights = []
- static_weights = []
- final = False
- pos = match.end()
- if not static:
- content += r"\Z"
- weight = Weighting(
- -len(static_weights),
- static_weights,
- -len(argument_weights),
- argument_weights,
- )
- yield RulePart(content=content, final=final, static=static, weight=weight)
- def compile(self) -> None:
- """Compiles the regular expression and stores it."""
- assert self.map is not None, "rule not bound"
- if self.map.host_matching:
- domain_rule = self.host or ""
- else:
- domain_rule = self.subdomain or ""
- self._parts = []
- self._trace = []
- self._converters = {}
- if domain_rule == "":
- self._parts = [
- RulePart(
- content="", final=False, static=True, weight=Weighting(0, [], 0, [])
- )
- ]
- else:
- self._parts.extend(self._parse_rule(domain_rule))
- self._trace.append((False, "|"))
- rule = self.rule
- if self.merge_slashes:
- rule = re.sub("/{2,}?", "/", self.rule)
- self._parts.extend(self._parse_rule(rule))
- self._build: t.Callable[..., t.Tuple[str, str]]
- self._build = self._compile_builder(False).__get__(self, None)
- self._build_unknown: t.Callable[..., t.Tuple[str, str]]
- self._build_unknown = self._compile_builder(True).__get__(self, None)
- @staticmethod
- def _get_func_code(code: CodeType, name: str) -> t.Callable[..., t.Tuple[str, str]]:
- globs: t.Dict[str, t.Any] = {}
- locs: t.Dict[str, t.Any] = {}
- exec(code, globs, locs)
- return locs[name] # type: ignore
- def _compile_builder(
- self, append_unknown: bool = True
- ) -> t.Callable[..., t.Tuple[str, str]]:
- defaults = self.defaults or {}
- dom_ops: t.List[t.Tuple[bool, str]] = []
- url_ops: t.List[t.Tuple[bool, str]] = []
- opl = dom_ops
- for is_dynamic, data in self._trace:
- if data == "|" and opl is dom_ops:
- opl = url_ops
- continue
- # this seems like a silly case to ever come up but:
- # if a default is given for a value that appears in the rule,
- # resolve it to a constant ahead of time
- if is_dynamic and data in defaults:
- data = self._converters[data].to_url(defaults[data])
- opl.append((False, data))
- elif not is_dynamic:
- opl.append(
- (False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+"))
- )
- else:
- opl.append((True, data))
- def _convert(elem: str) -> ast.stmt:
- ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem))
- ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2
- return ret
- def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]:
- parts = [
- _convert(elem) if is_dynamic else ast.Str(s=elem)
- for is_dynamic, elem in ops
- ]
- parts = parts or [ast.Str("")]
- # constant fold
- ret = [parts[0]]
- for p in parts[1:]:
- if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str):
- ret[-1] = ast.Str(ret[-1].s + p.s)
- else:
- ret.append(p)
- return ret
- dom_parts = _parts(dom_ops)
- url_parts = _parts(url_ops)
- if not append_unknown:
- body = []
- else:
- body = [_IF_KWARGS_URL_ENCODE_AST]
- url_parts.extend(_URL_ENCODE_AST_NAMES)
- def _join(parts: t.List[ast.AST]) -> ast.AST:
- if len(parts) == 1: # shortcut
- return parts[0]
- return ast.JoinedStr(parts)
- body.append(
- ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load()))
- )
- pargs = [
- elem
- for is_dynamic, elem in dom_ops + url_ops
- if is_dynamic and elem not in defaults
- ]
- kargs = [str(k) for k in defaults]
- func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore
- func_ast.name = f"<builder:{self.rule!r}>"
- func_ast.args.args.append(ast.arg(".self", None))
- for arg in pargs + kargs:
- func_ast.args.args.append(ast.arg(arg, None))
- func_ast.args.kwarg = ast.arg(".kwargs", None)
- for _ in kargs:
- func_ast.args.defaults.append(ast.Str(""))
- func_ast.body = body
- # use `ast.parse` instead of `ast.Module` for better portability
- # Python 3.8 changes the signature of `ast.Module`
- module = ast.parse("")
- module.body = [func_ast]
- # mark everything as on line 1, offset 0
- # less error-prone than `ast.fix_missing_locations`
- # bad line numbers cause an assert to fail in debug builds
- for node in ast.walk(module):
- if "lineno" in node._attributes:
- node.lineno = 1
- if "end_lineno" in node._attributes:
- node.end_lineno = node.lineno # type: ignore[attr-defined]
- if "col_offset" in node._attributes:
- node.col_offset = 0
- if "end_col_offset" in node._attributes:
- node.end_col_offset = node.col_offset # type: ignore[attr-defined]
- code = compile(module, "<werkzeug routing>", "exec")
- return self._get_func_code(code, func_ast.name)
- def build(
- self, values: t.Mapping[str, t.Any], append_unknown: bool = True
- ) -> t.Optional[t.Tuple[str, str]]:
- """Assembles the relative url for that rule and the subdomain.
- If building doesn't work for some reasons `None` is returned.
- :internal:
- """
- try:
- if append_unknown:
- return self._build_unknown(**values)
- else:
- return self._build(**values)
- except ValidationError:
- return None
- def provides_defaults_for(self, rule: "Rule") -> bool:
- """Check if this rule has defaults for a given rule.
- :internal:
- """
- return bool(
- not self.build_only
- and self.defaults
- and self.endpoint == rule.endpoint
- and self != rule
- and self.arguments == rule.arguments
- )
- def suitable_for(
- self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None
- ) -> bool:
- """Check if the dict of values has enough data for url generation.
- :internal:
- """
- # if a method was given explicitly and that method is not supported
- # by this rule, this rule is not suitable.
- if (
- method is not None
- and self.methods is not None
- and method not in self.methods
- ):
- return False
- defaults = self.defaults or ()
- # all arguments required must be either in the defaults dict or
- # the value dictionary otherwise it's not suitable
- for key in self.arguments:
- if key not in defaults and key not in values:
- return False
- # in case defaults are given we ensure that either the value was
- # skipped or the value is the same as the default value.
- if defaults:
- for key, value in defaults.items():
- if key in values and value != values[key]:
- return False
- return True
- def build_compare_key(self) -> t.Tuple[int, int, int]:
- """The build compare key for sorting.
- :internal:
- """
- return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ()))
- def __eq__(self, other: object) -> bool:
- return isinstance(other, type(self)) and self._trace == other._trace
- __hash__ = None # type: ignore
- def __str__(self) -> str:
- return self.rule
- def __repr__(self) -> str:
- if self.map is None:
- return f"<{type(self).__name__} (unbound)>"
- parts = []
- for is_dynamic, data in self._trace:
- if is_dynamic:
- parts.append(f"<{data}>")
- else:
- parts.append(data)
- parts = "".join(parts).lstrip("|")
- methods = f" ({', '.join(self.methods)})" if self.methods is not None else ""
- return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>"
|