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