Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/routing/map.py: 50%
319 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-09 06:08 +0000
1from __future__ import annotations
3import typing as t
4import warnings
5from pprint import pformat
6from threading import Lock
7from urllib.parse import quote
8from urllib.parse import urljoin
9from urllib.parse import urlunsplit
11from .._internal import _get_environ
12from .._internal import _wsgi_decoding_dance
13from ..datastructures import ImmutableDict
14from ..datastructures import MultiDict
15from ..exceptions import BadHost
16from ..exceptions import HTTPException
17from ..exceptions import MethodNotAllowed
18from ..exceptions import NotFound
19from ..urls import _urlencode
20from ..wsgi import get_host
21from .converters import DEFAULT_CONVERTERS
22from .exceptions import BuildError
23from .exceptions import NoMatch
24from .exceptions import RequestAliasRedirect
25from .exceptions import RequestPath
26from .exceptions import RequestRedirect
27from .exceptions import WebsocketMismatch
28from .matcher import StateMachineMatcher
29from .rules import _simple_rule_re
30from .rules import Rule
32if t.TYPE_CHECKING:
33 from _typeshed.wsgi import WSGIApplication
34 from _typeshed.wsgi import WSGIEnvironment
35 from .converters import BaseConverter
36 from .rules import RuleFactory
37 from ..wrappers.request import Request
40class Map:
41 """The map class stores all the URL rules and some configuration
42 parameters. Some of the configuration values are only stored on the
43 `Map` instance since those affect all rules, others are just defaults
44 and can be overridden for each rule. Note that you have to specify all
45 arguments besides the `rules` as keyword arguments!
47 :param rules: sequence of url rules for this map.
48 :param default_subdomain: The default subdomain for rules without a
49 subdomain defined.
50 :param charset: charset of the url. defaults to ``"utf-8"``
51 :param strict_slashes: If a rule ends with a slash but the matched
52 URL does not, redirect to the URL with a trailing slash.
53 :param merge_slashes: Merge consecutive slashes when matching or
54 building URLs. Matches will redirect to the normalized URL.
55 Slashes in variable parts are not merged.
56 :param redirect_defaults: This will redirect to the default rule if it
57 wasn't visited that way. This helps creating
58 unique URLs.
59 :param converters: A dict of converters that adds additional converters
60 to the list of converters. If you redefine one
61 converter this will override the original one.
62 :param sort_parameters: If set to `True` the url parameters are sorted.
63 See `url_encode` for more details.
64 :param sort_key: The sort key function for `url_encode`.
65 :param encoding_errors: the error method to use for decoding
66 :param host_matching: if set to `True` it enables the host matching
67 feature and disables the subdomain one. If
68 enabled the `host` parameter to rules is used
69 instead of the `subdomain` one.
71 .. versionchanged:: 2.3
72 The ``charset`` and ``encoding_errors`` parameters are deprecated and will be
73 removed in Werkzeug 3.0.
75 .. versionchanged:: 1.0
76 If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match.
78 .. versionchanged:: 1.0
79 The ``merge_slashes`` parameter was added.
81 .. versionchanged:: 0.7
82 The ``encoding_errors`` and ``host_matching`` parameters were added.
84 .. versionchanged:: 0.5
85 The ``sort_parameters`` and ``sort_key`` paramters were added.
86 """
88 #: A dict of default converters to be used.
89 default_converters = ImmutableDict(DEFAULT_CONVERTERS)
91 #: The type of lock to use when updating.
92 #:
93 #: .. versionadded:: 1.0
94 lock_class = Lock
96 def __init__(
97 self,
98 rules: t.Iterable[RuleFactory] | None = None,
99 default_subdomain: str = "",
100 charset: str | None = None,
101 strict_slashes: bool = True,
102 merge_slashes: bool = True,
103 redirect_defaults: bool = True,
104 converters: t.Mapping[str, type[BaseConverter]] | None = None,
105 sort_parameters: bool = False,
106 sort_key: t.Callable[[t.Any], t.Any] | None = None,
107 encoding_errors: str | None = None,
108 host_matching: bool = False,
109 ) -> None:
110 self._matcher = StateMachineMatcher(merge_slashes)
111 self._rules_by_endpoint: dict[str, list[Rule]] = {}
112 self._remap = True
113 self._remap_lock = self.lock_class()
115 self.default_subdomain = default_subdomain
117 if charset is not None:
118 warnings.warn(
119 "The 'charset' parameter is deprecated and will be"
120 " removed in Werkzeug 3.0.",
121 DeprecationWarning,
122 stacklevel=2,
123 )
124 else:
125 charset = "utf-8"
127 self.charset = charset
129 if encoding_errors is not None:
130 warnings.warn(
131 "The 'encoding_errors' parameter is deprecated and will be"
132 " removed in Werkzeug 3.0.",
133 DeprecationWarning,
134 stacklevel=2,
135 )
136 else:
137 encoding_errors = "replace"
139 self.encoding_errors = encoding_errors
140 self.strict_slashes = strict_slashes
141 self.merge_slashes = merge_slashes
142 self.redirect_defaults = redirect_defaults
143 self.host_matching = host_matching
145 self.converters = self.default_converters.copy()
146 if converters:
147 self.converters.update(converters)
149 self.sort_parameters = sort_parameters
150 self.sort_key = sort_key
152 for rulefactory in rules or ():
153 self.add(rulefactory)
155 def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool:
156 """Iterate over all rules and check if the endpoint expects
157 the arguments provided. This is for example useful if you have
158 some URLs that expect a language code and others that do not and
159 you want to wrap the builder a bit so that the current language
160 code is automatically added if not provided but endpoints expect
161 it.
163 :param endpoint: the endpoint to check.
164 :param arguments: this function accepts one or more arguments
165 as positional arguments. Each one of them is
166 checked.
167 """
168 self.update()
169 arguments = set(arguments)
170 for rule in self._rules_by_endpoint[endpoint]:
171 if arguments.issubset(rule.arguments):
172 return True
173 return False
175 @property
176 def _rules(self) -> list[Rule]:
177 return [rule for rules in self._rules_by_endpoint.values() for rule in rules]
179 def iter_rules(self, endpoint: str | None = None) -> t.Iterator[Rule]:
180 """Iterate over all rules or the rules of an endpoint.
182 :param endpoint: if provided only the rules for that endpoint
183 are returned.
184 :return: an iterator
185 """
186 self.update()
187 if endpoint is not None:
188 return iter(self._rules_by_endpoint[endpoint])
189 return iter(self._rules)
191 def add(self, rulefactory: RuleFactory) -> None:
192 """Add a new rule or factory to the map and bind it. Requires that the
193 rule is not bound to another map.
195 :param rulefactory: a :class:`Rule` or :class:`RuleFactory`
196 """
197 for rule in rulefactory.get_rules(self):
198 rule.bind(self)
199 if not rule.build_only:
200 self._matcher.add(rule)
201 self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
202 self._remap = True
204 def bind(
205 self,
206 server_name: str,
207 script_name: str | None = None,
208 subdomain: str | None = None,
209 url_scheme: str = "http",
210 default_method: str = "GET",
211 path_info: str | None = None,
212 query_args: t.Mapping[str, t.Any] | str | None = None,
213 ) -> MapAdapter:
214 """Return a new :class:`MapAdapter` with the details specified to the
215 call. Note that `script_name` will default to ``'/'`` if not further
216 specified or `None`. The `server_name` at least is a requirement
217 because the HTTP RFC requires absolute URLs for redirects and so all
218 redirect exceptions raised by Werkzeug will contain the full canonical
219 URL.
221 If no path_info is passed to :meth:`match` it will use the default path
222 info passed to bind. While this doesn't really make sense for
223 manual bind calls, it's useful if you bind a map to a WSGI
224 environment which already contains the path info.
226 `subdomain` will default to the `default_subdomain` for this map if
227 no defined. If there is no `default_subdomain` you cannot use the
228 subdomain feature.
230 .. versionchanged:: 1.0
231 If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules
232 will match.
234 .. versionchanged:: 0.15
235 ``path_info`` defaults to ``'/'`` if ``None``.
237 .. versionchanged:: 0.8
238 ``query_args`` can be a string.
240 .. versionchanged:: 0.7
241 Added ``query_args``.
242 """
243 server_name = server_name.lower()
244 if self.host_matching:
245 if subdomain is not None:
246 raise RuntimeError("host matching enabled and a subdomain was provided")
247 elif subdomain is None:
248 subdomain = self.default_subdomain
249 if script_name is None:
250 script_name = "/"
251 if path_info is None:
252 path_info = "/"
254 # Port isn't part of IDNA, and might push a name over the 63 octet limit.
255 server_name, port_sep, port = server_name.partition(":")
257 try:
258 server_name = server_name.encode("idna").decode("ascii")
259 except UnicodeError as e:
260 raise BadHost() from e
262 return MapAdapter(
263 self,
264 f"{server_name}{port_sep}{port}",
265 script_name,
266 subdomain,
267 url_scheme,
268 path_info,
269 default_method,
270 query_args,
271 )
273 def bind_to_environ(
274 self,
275 environ: WSGIEnvironment | Request,
276 server_name: str | None = None,
277 subdomain: str | None = None,
278 ) -> MapAdapter:
279 """Like :meth:`bind` but you can pass it an WSGI environment and it
280 will fetch the information from that dictionary. Note that because of
281 limitations in the protocol there is no way to get the current
282 subdomain and real `server_name` from the environment. If you don't
283 provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or
284 `HTTP_HOST` if provided) as used `server_name` with disabled subdomain
285 feature.
287 If `subdomain` is `None` but an environment and a server name is
288 provided it will calculate the current subdomain automatically.
289 Example: `server_name` is ``'example.com'`` and the `SERVER_NAME`
290 in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated
291 subdomain will be ``'staging.dev'``.
293 If the object passed as environ has an environ attribute, the value of
294 this attribute is used instead. This allows you to pass request
295 objects. Additionally `PATH_INFO` added as a default of the
296 :class:`MapAdapter` so that you don't have to pass the path info to
297 the match method.
299 .. versionchanged:: 1.0.0
300 If the passed server name specifies port 443, it will match
301 if the incoming scheme is ``https`` without a port.
303 .. versionchanged:: 1.0.0
304 A warning is shown when the passed server name does not
305 match the incoming WSGI server name.
307 .. versionchanged:: 0.8
308 This will no longer raise a ValueError when an unexpected server
309 name was passed.
311 .. versionchanged:: 0.5
312 previously this method accepted a bogus `calculate_subdomain`
313 parameter that did not have any effect. It was removed because
314 of that.
316 :param environ: a WSGI environment.
317 :param server_name: an optional server name hint (see above).
318 :param subdomain: optionally the current subdomain (see above).
319 """
320 env = _get_environ(environ)
321 wsgi_server_name = get_host(env).lower()
322 scheme = env["wsgi.url_scheme"]
323 upgrade = any(
324 v.strip() == "upgrade"
325 for v in env.get("HTTP_CONNECTION", "").lower().split(",")
326 )
328 if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket":
329 scheme = "wss" if scheme == "https" else "ws"
331 if server_name is None:
332 server_name = wsgi_server_name
333 else:
334 server_name = server_name.lower()
336 # strip standard port to match get_host()
337 if scheme in {"http", "ws"} and server_name.endswith(":80"):
338 server_name = server_name[:-3]
339 elif scheme in {"https", "wss"} and server_name.endswith(":443"):
340 server_name = server_name[:-4]
342 if subdomain is None and not self.host_matching:
343 cur_server_name = wsgi_server_name.split(".")
344 real_server_name = server_name.split(".")
345 offset = -len(real_server_name)
347 if cur_server_name[offset:] != real_server_name:
348 # This can happen even with valid configs if the server was
349 # accessed directly by IP address under some situations.
350 # Instead of raising an exception like in Werkzeug 0.7 or
351 # earlier we go by an invalid subdomain which will result
352 # in a 404 error on matching.
353 warnings.warn(
354 f"Current server name {wsgi_server_name!r} doesn't match configured"
355 f" server name {server_name!r}",
356 stacklevel=2,
357 )
358 subdomain = "<invalid>"
359 else:
360 subdomain = ".".join(filter(None, cur_server_name[:offset]))
362 def _get_wsgi_string(name: str) -> str | None:
363 val = env.get(name)
364 if val is not None:
365 return _wsgi_decoding_dance(val, self.charset)
366 return None
368 script_name = _get_wsgi_string("SCRIPT_NAME")
369 path_info = _get_wsgi_string("PATH_INFO")
370 query_args = _get_wsgi_string("QUERY_STRING")
371 return Map.bind(
372 self,
373 server_name,
374 script_name,
375 subdomain,
376 scheme,
377 env["REQUEST_METHOD"],
378 path_info,
379 query_args=query_args,
380 )
382 def update(self) -> None:
383 """Called before matching and building to keep the compiled rules
384 in the correct order after things changed.
385 """
386 if not self._remap:
387 return
389 with self._remap_lock:
390 if not self._remap:
391 return
393 self._matcher.update()
394 for rules in self._rules_by_endpoint.values():
395 rules.sort(key=lambda x: x.build_compare_key())
396 self._remap = False
398 def __repr__(self) -> str:
399 rules = self.iter_rules()
400 return f"{type(self).__name__}({pformat(list(rules))})"
403class MapAdapter:
405 """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does
406 the URL matching and building based on runtime information.
407 """
409 def __init__(
410 self,
411 map: Map,
412 server_name: str,
413 script_name: str,
414 subdomain: str | None,
415 url_scheme: str,
416 path_info: str,
417 default_method: str,
418 query_args: t.Mapping[str, t.Any] | str | None = None,
419 ):
420 self.map = map
421 self.server_name = server_name
423 if not script_name.endswith("/"):
424 script_name += "/"
426 self.script_name = script_name
427 self.subdomain = subdomain
428 self.url_scheme = url_scheme
429 self.path_info = path_info
430 self.default_method = default_method
431 self.query_args = query_args
432 self.websocket = self.url_scheme in {"ws", "wss"}
434 def dispatch(
435 self,
436 view_func: t.Callable[[str, t.Mapping[str, t.Any]], WSGIApplication],
437 path_info: str | None = None,
438 method: str | None = None,
439 catch_http_exceptions: bool = False,
440 ) -> WSGIApplication:
441 """Does the complete dispatching process. `view_func` is called with
442 the endpoint and a dict with the values for the view. It should
443 look up the view function, call it, and return a response object
444 or WSGI application. http exceptions are not caught by default
445 so that applications can display nicer error messages by just
446 catching them by hand. If you want to stick with the default
447 error messages you can pass it ``catch_http_exceptions=True`` and
448 it will catch the http exceptions.
450 Here a small example for the dispatch usage::
452 from werkzeug.wrappers import Request, Response
453 from werkzeug.wsgi import responder
454 from werkzeug.routing import Map, Rule
456 def on_index(request):
457 return Response('Hello from the index')
459 url_map = Map([Rule('/', endpoint='index')])
460 views = {'index': on_index}
462 @responder
463 def application(environ, start_response):
464 request = Request(environ)
465 urls = url_map.bind_to_environ(environ)
466 return urls.dispatch(lambda e, v: views[e](request, **v),
467 catch_http_exceptions=True)
469 Keep in mind that this method might return exception objects, too, so
470 use :class:`Response.force_type` to get a response object.
472 :param view_func: a function that is called with the endpoint as
473 first argument and the value dict as second. Has
474 to dispatch to the actual view function with this
475 information. (see above)
476 :param path_info: the path info to use for matching. Overrides the
477 path info specified on binding.
478 :param method: the HTTP method used for matching. Overrides the
479 method specified on binding.
480 :param catch_http_exceptions: set to `True` to catch any of the
481 werkzeug :class:`HTTPException`\\s.
482 """
483 try:
484 try:
485 endpoint, args = self.match(path_info, method)
486 except RequestRedirect as e:
487 return e
488 return view_func(endpoint, args)
489 except HTTPException as e:
490 if catch_http_exceptions:
491 return e
492 raise
494 @t.overload
495 def match( # type: ignore
496 self,
497 path_info: str | None = None,
498 method: str | None = None,
499 return_rule: t.Literal[False] = False,
500 query_args: t.Mapping[str, t.Any] | str | None = None,
501 websocket: bool | None = None,
502 ) -> tuple[str, t.Mapping[str, t.Any]]:
503 ...
505 @t.overload
506 def match(
507 self,
508 path_info: str | None = None,
509 method: str | None = None,
510 return_rule: t.Literal[True] = True,
511 query_args: t.Mapping[str, t.Any] | str | None = None,
512 websocket: bool | None = None,
513 ) -> tuple[Rule, t.Mapping[str, t.Any]]:
514 ...
516 def match(
517 self,
518 path_info: str | None = None,
519 method: str | None = None,
520 return_rule: bool = False,
521 query_args: t.Mapping[str, t.Any] | str | None = None,
522 websocket: bool | None = None,
523 ) -> tuple[str | Rule, t.Mapping[str, t.Any]]:
524 """The usage is simple: you just pass the match method the current
525 path info as well as the method (which defaults to `GET`). The
526 following things can then happen:
528 - you receive a `NotFound` exception that indicates that no URL is
529 matching. A `NotFound` exception is also a WSGI application you
530 can call to get a default page not found page (happens to be the
531 same object as `werkzeug.exceptions.NotFound`)
533 - you receive a `MethodNotAllowed` exception that indicates that there
534 is a match for this URL but not for the current request method.
535 This is useful for RESTful applications.
537 - you receive a `RequestRedirect` exception with a `new_url`
538 attribute. This exception is used to notify you about a request
539 Werkzeug requests from your WSGI application. This is for example the
540 case if you request ``/foo`` although the correct URL is ``/foo/``
541 You can use the `RequestRedirect` instance as response-like object
542 similar to all other subclasses of `HTTPException`.
544 - you receive a ``WebsocketMismatch`` exception if the only
545 match is a WebSocket rule but the bind is an HTTP request, or
546 if the match is an HTTP rule but the bind is a WebSocket
547 request.
549 - you get a tuple in the form ``(endpoint, arguments)`` if there is
550 a match (unless `return_rule` is True, in which case you get a tuple
551 in the form ``(rule, arguments)``)
553 If the path info is not passed to the match method the default path
554 info of the map is used (defaults to the root URL if not defined
555 explicitly).
557 All of the exceptions raised are subclasses of `HTTPException` so they
558 can be used as WSGI responses. They will all render generic error or
559 redirect pages.
561 Here is a small example for matching:
563 >>> m = Map([
564 ... Rule('/', endpoint='index'),
565 ... Rule('/downloads/', endpoint='downloads/index'),
566 ... Rule('/downloads/<int:id>', endpoint='downloads/show')
567 ... ])
568 >>> urls = m.bind("example.com", "/")
569 >>> urls.match("/", "GET")
570 ('index', {})
571 >>> urls.match("/downloads/42")
572 ('downloads/show', {'id': 42})
574 And here is what happens on redirect and missing URLs:
576 >>> urls.match("/downloads")
577 Traceback (most recent call last):
578 ...
579 RequestRedirect: http://example.com/downloads/
580 >>> urls.match("/missing")
581 Traceback (most recent call last):
582 ...
583 NotFound: 404 Not Found
585 :param path_info: the path info to use for matching. Overrides the
586 path info specified on binding.
587 :param method: the HTTP method used for matching. Overrides the
588 method specified on binding.
589 :param return_rule: return the rule that matched instead of just the
590 endpoint (defaults to `False`).
591 :param query_args: optional query arguments that are used for
592 automatic redirects as string or dictionary. It's
593 currently not possible to use the query arguments
594 for URL matching.
595 :param websocket: Match WebSocket instead of HTTP requests. A
596 websocket request has a ``ws`` or ``wss``
597 :attr:`url_scheme`. This overrides that detection.
599 .. versionadded:: 1.0
600 Added ``websocket``.
602 .. versionchanged:: 0.8
603 ``query_args`` can be a string.
605 .. versionadded:: 0.7
606 Added ``query_args``.
608 .. versionadded:: 0.6
609 Added ``return_rule``.
610 """
611 self.map.update()
612 if path_info is None:
613 path_info = self.path_info
614 if query_args is None:
615 query_args = self.query_args or {}
616 method = (method or self.default_method).upper()
618 if websocket is None:
619 websocket = self.websocket
621 domain_part = self.server_name
623 if not self.map.host_matching and self.subdomain is not None:
624 domain_part = self.subdomain
626 path_part = f"/{path_info.lstrip('/')}" if path_info else ""
628 try:
629 result = self.map._matcher.match(domain_part, path_part, method, websocket)
630 except RequestPath as e:
631 # safe = https://url.spec.whatwg.org/#url-path-segment-string
632 new_path = quote(
633 e.path_info, safe="!$&'()*+,/:;=@", encoding=self.map.charset
634 )
635 raise RequestRedirect(
636 self.make_redirect_url(new_path, query_args)
637 ) from None
638 except RequestAliasRedirect as e:
639 raise RequestRedirect(
640 self.make_alias_redirect_url(
641 f"{domain_part}|{path_part}",
642 e.endpoint,
643 e.matched_values,
644 method,
645 query_args,
646 )
647 ) from None
648 except NoMatch as e:
649 if e.have_match_for:
650 raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None
652 if e.websocket_mismatch:
653 raise WebsocketMismatch() from None
655 raise NotFound() from None
656 else:
657 rule, rv = result
659 if self.map.redirect_defaults:
660 redirect_url = self.get_default_redirect(rule, method, rv, query_args)
661 if redirect_url is not None:
662 raise RequestRedirect(redirect_url)
664 if rule.redirect_to is not None:
665 if isinstance(rule.redirect_to, str):
667 def _handle_match(match: t.Match[str]) -> str:
668 value = rv[match.group(1)]
669 return rule._converters[match.group(1)].to_url(value)
671 redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to)
672 else:
673 redirect_url = rule.redirect_to(self, **rv)
675 if self.subdomain:
676 netloc = f"{self.subdomain}.{self.server_name}"
677 else:
678 netloc = self.server_name
680 raise RequestRedirect(
681 urljoin(
682 f"{self.url_scheme or 'http'}://{netloc}{self.script_name}",
683 redirect_url,
684 )
685 )
687 if return_rule:
688 return rule, rv
689 else:
690 return rule.endpoint, rv
692 def test(self, path_info: str | None = None, method: str | None = None) -> bool:
693 """Test if a rule would match. Works like `match` but returns `True`
694 if the URL matches, or `False` if it does not exist.
696 :param path_info: the path info to use for matching. Overrides the
697 path info specified on binding.
698 :param method: the HTTP method used for matching. Overrides the
699 method specified on binding.
700 """
701 try:
702 self.match(path_info, method)
703 except RequestRedirect:
704 pass
705 except HTTPException:
706 return False
707 return True
709 def allowed_methods(self, path_info: str | None = None) -> t.Iterable[str]:
710 """Returns the valid methods that match for a given path.
712 .. versionadded:: 0.7
713 """
714 try:
715 self.match(path_info, method="--")
716 except MethodNotAllowed as e:
717 return e.valid_methods # type: ignore
718 except HTTPException:
719 pass
720 return []
722 def get_host(self, domain_part: str | None) -> str:
723 """Figures out the full host name for the given domain part. The
724 domain part is a subdomain in case host matching is disabled or
725 a full host name.
726 """
727 if self.map.host_matching:
728 if domain_part is None:
729 return self.server_name
731 return domain_part
733 if domain_part is None:
734 subdomain = self.subdomain
735 else:
736 subdomain = domain_part
738 if subdomain:
739 return f"{subdomain}.{self.server_name}"
740 else:
741 return self.server_name
743 def get_default_redirect(
744 self,
745 rule: Rule,
746 method: str,
747 values: t.MutableMapping[str, t.Any],
748 query_args: t.Mapping[str, t.Any] | str,
749 ) -> str | None:
750 """A helper that returns the URL to redirect to if it finds one.
751 This is used for default redirecting only.
753 :internal:
754 """
755 assert self.map.redirect_defaults
756 for r in self.map._rules_by_endpoint[rule.endpoint]:
757 # every rule that comes after this one, including ourself
758 # has a lower priority for the defaults. We order the ones
759 # with the highest priority up for building.
760 if r is rule:
761 break
762 if r.provides_defaults_for(rule) and r.suitable_for(values, method):
763 values.update(r.defaults) # type: ignore
764 domain_part, path = r.build(values) # type: ignore
765 return self.make_redirect_url(path, query_args, domain_part=domain_part)
766 return None
768 def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str:
769 if not isinstance(query_args, str):
770 return _urlencode(query_args, encoding=self.map.charset)
771 return query_args
773 def make_redirect_url(
774 self,
775 path_info: str,
776 query_args: t.Mapping[str, t.Any] | str | None = None,
777 domain_part: str | None = None,
778 ) -> str:
779 """Creates a redirect URL.
781 :internal:
782 """
783 if query_args is None:
784 query_args = self.query_args
786 if query_args:
787 query_str = self.encode_query_args(query_args)
788 else:
789 query_str = None
791 scheme = self.url_scheme or "http"
792 host = self.get_host(domain_part)
793 path = "/".join((self.script_name.strip("/"), path_info.lstrip("/")))
794 return urlunsplit((scheme, host, path, query_str, None))
796 def make_alias_redirect_url(
797 self,
798 path: str,
799 endpoint: str,
800 values: t.Mapping[str, t.Any],
801 method: str,
802 query_args: t.Mapping[str, t.Any] | str,
803 ) -> str:
804 """Internally called to make an alias redirect URL."""
805 url = self.build(
806 endpoint, values, method, append_unknown=False, force_external=True
807 )
808 if query_args:
809 url += f"?{self.encode_query_args(query_args)}"
810 assert url != path, "detected invalid alias setting. No canonical URL found"
811 return url
813 def _partial_build(
814 self,
815 endpoint: str,
816 values: t.Mapping[str, t.Any],
817 method: str | None,
818 append_unknown: bool,
819 ) -> tuple[str, str, bool] | None:
820 """Helper for :meth:`build`. Returns subdomain and path for the
821 rule that accepts this endpoint, values and method.
823 :internal:
824 """
825 # in case the method is none, try with the default method first
826 if method is None:
827 rv = self._partial_build(
828 endpoint, values, self.default_method, append_unknown
829 )
830 if rv is not None:
831 return rv
833 # Default method did not match or a specific method is passed.
834 # Check all for first match with matching host. If no matching
835 # host is found, go with first result.
836 first_match = None
838 for rule in self.map._rules_by_endpoint.get(endpoint, ()):
839 if rule.suitable_for(values, method):
840 build_rv = rule.build(values, append_unknown)
842 if build_rv is not None:
843 rv = (build_rv[0], build_rv[1], rule.websocket)
844 if self.map.host_matching:
845 if rv[0] == self.server_name:
846 return rv
847 elif first_match is None:
848 first_match = rv
849 else:
850 return rv
852 return first_match
854 def build(
855 self,
856 endpoint: str,
857 values: t.Mapping[str, t.Any] | None = None,
858 method: str | None = None,
859 force_external: bool = False,
860 append_unknown: bool = True,
861 url_scheme: str | None = None,
862 ) -> str:
863 """Building URLs works pretty much the other way round. Instead of
864 `match` you call `build` and pass it the endpoint and a dict of
865 arguments for the placeholders.
867 The `build` function also accepts an argument called `force_external`
868 which, if you set it to `True` will force external URLs. Per default
869 external URLs (include the server name) will only be used if the
870 target URL is on a different subdomain.
872 >>> m = Map([
873 ... Rule('/', endpoint='index'),
874 ... Rule('/downloads/', endpoint='downloads/index'),
875 ... Rule('/downloads/<int:id>', endpoint='downloads/show')
876 ... ])
877 >>> urls = m.bind("example.com", "/")
878 >>> urls.build("index", {})
879 '/'
880 >>> urls.build("downloads/show", {'id': 42})
881 '/downloads/42'
882 >>> urls.build("downloads/show", {'id': 42}, force_external=True)
883 'http://example.com/downloads/42'
885 Because URLs cannot contain non ASCII data you will always get
886 bytes back. Non ASCII characters are urlencoded with the
887 charset defined on the map instance.
889 Additional values are converted to strings and appended to the URL as
890 URL querystring parameters:
892 >>> urls.build("index", {'q': 'My Searchstring'})
893 '/?q=My+Searchstring'
895 When processing those additional values, lists are furthermore
896 interpreted as multiple values (as per
897 :py:class:`werkzeug.datastructures.MultiDict`):
899 >>> urls.build("index", {'q': ['a', 'b', 'c']})
900 '/?q=a&q=b&q=c'
902 Passing a ``MultiDict`` will also add multiple values:
904 >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b'))))
905 '/?p=z&q=a&q=b'
907 If a rule does not exist when building a `BuildError` exception is
908 raised.
910 The build method accepts an argument called `method` which allows you
911 to specify the method you want to have an URL built for if you have
912 different methods for the same endpoint specified.
914 :param endpoint: the endpoint of the URL to build.
915 :param values: the values for the URL to build. Unhandled values are
916 appended to the URL as query parameters.
917 :param method: the HTTP method for the rule if there are different
918 URLs for different methods on the same endpoint.
919 :param force_external: enforce full canonical external URLs. If the URL
920 scheme is not provided, this will generate
921 a protocol-relative URL.
922 :param append_unknown: unknown parameters are appended to the generated
923 URL as query string argument. Disable this
924 if you want the builder to ignore those.
925 :param url_scheme: Scheme to use in place of the bound
926 :attr:`url_scheme`.
928 .. versionchanged:: 2.0
929 Added the ``url_scheme`` parameter.
931 .. versionadded:: 0.6
932 Added the ``append_unknown`` parameter.
933 """
934 self.map.update()
936 if values:
937 if isinstance(values, MultiDict):
938 values = {
939 k: (v[0] if len(v) == 1 else v)
940 for k, v in dict.items(values)
941 if len(v) != 0
942 }
943 else: # plain dict
944 values = {k: v for k, v in values.items() if v is not None}
945 else:
946 values = {}
948 rv = self._partial_build(endpoint, values, method, append_unknown)
949 if rv is None:
950 raise BuildError(endpoint, values, method, self)
952 domain_part, path, websocket = rv
953 host = self.get_host(domain_part)
955 if url_scheme is None:
956 url_scheme = self.url_scheme
958 # Always build WebSocket routes with the scheme (browsers
959 # require full URLs). If bound to a WebSocket, ensure that HTTP
960 # routes are built with an HTTP scheme.
961 secure = url_scheme in {"https", "wss"}
963 if websocket:
964 force_external = True
965 url_scheme = "wss" if secure else "ws"
966 elif url_scheme:
967 url_scheme = "https" if secure else "http"
969 # shortcut this.
970 if not force_external and (
971 (self.map.host_matching and host == self.server_name)
972 or (not self.map.host_matching and domain_part == self.subdomain)
973 ):
974 return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}"
976 scheme = f"{url_scheme}:" if url_scheme else ""
977 return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}"