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