Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
map.py929 linesDownload Raw Back to routing
1from __future__ import annotations2 3import typing as t4import warnings5from pprint import pformat6from threading import Lock7from urllib.parse import quote8from urllib.parse import urljoin9from urllib.parse import urlunsplit10 11from .._internal import _get_environ12from .._internal import _wsgi_decoding_dance13from ..datastructures import ImmutableDict14from ..datastructures import MultiDict15from ..exceptions import BadHost16from ..exceptions import HTTPException17from ..exceptions import MethodNotAllowed18from ..exceptions import NotFound19from ..urls import _urlencode20from ..wsgi import get_host21from .converters import DEFAULT_CONVERTERS22from .exceptions import BuildError23from .exceptions import NoMatch24from .exceptions import RequestAliasRedirect25from .exceptions import RequestPath26from .exceptions import RequestRedirect27from .exceptions import WebsocketMismatch28from .matcher import StateMachineMatcher29from .rules import _simple_rule_re30from .rules import Rule31 32if t.TYPE_CHECKING:33    from _typeshed.wsgi import WSGIApplication34    from _typeshed.wsgi import WSGIEnvironment35 36    from ..wrappers.request import Request37    from .converters import BaseConverter38    from .rules import RuleFactory39 40 41class Map:42    """The map class stores all the URL rules and some configuration43    parameters.  Some of the configuration values are only stored on the44    `Map` instance since those affect all rules, others are just defaults45    and can be overridden for each rule.  Note that you have to specify all46    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 a50                              subdomain defined.51    :param strict_slashes: If a rule ends with a slash but the matched52        URL does not, redirect to the URL with a trailing slash.53    :param merge_slashes: Merge consecutive slashes when matching or54        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 it57                              wasn't visited that way. This helps creating58                              unique URLs.59    :param converters: A dict of converters that adds additional converters60                       to the list of converters. If you redefine one61                       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 matching66                          feature and disables the subdomain one.  If67                          enabled the `host` parameter to rules is used68                          instead of the `subdomain` one.69 70    .. versionchanged:: 3.071        The ``charset`` and ``encoding_errors`` parameters were removed.72 73    .. versionchanged:: 1.074        If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match.75 76    .. versionchanged:: 1.077        The ``merge_slashes`` parameter was added.78 79    .. versionchanged:: 0.780        The ``encoding_errors`` and ``host_matching`` parameters were added.81 82    .. versionchanged:: 0.583        The ``sort_parameters`` and ``sort_key``  parameters 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.092    lock_class = Lock93 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 = True109        self._remap_lock = self.lock_class()110 111        self.default_subdomain = default_subdomain112        self.strict_slashes = strict_slashes113        self.redirect_defaults = redirect_defaults114        self.host_matching = host_matching115 116        self.converters = self.default_converters.copy()117        if converters:118            self.converters.update(converters)119 120        self.sort_parameters = sort_parameters121        self.sort_key = sort_key122 123        for rulefactory in rules or ():124            self.add(rulefactory)125 126    @property127    def merge_slashes(self) -> bool:128        return self._matcher.merge_slashes129 130    @merge_slashes.setter131    def merge_slashes(self, value: bool) -> None:132        self._matcher.merge_slashes = value133 134    def is_endpoint_expecting(self, endpoint: t.Any, *arguments: str) -> bool:135        """Iterate over all rules and check if the endpoint expects136        the arguments provided.  This is for example useful if you have137        some URLs that expect a language code and others that do not and138        you want to wrap the builder a bit so that the current language139        code is automatically added if not provided but endpoints expect140        it.141 142        :param endpoint: the endpoint to check.143        :param arguments: this function accepts one or more arguments144                          as positional arguments.  Each one of them is145                          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 True152        return False153 154    @property155    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 endpoint162                         are returned.163        :return: an iterator164        """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 the172        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 = True182 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 the194        call.  Note that `script_name` will default to ``'/'`` if not further195        specified or `None`.  The `server_name` at least is a requirement196        because the HTTP RFC requires absolute URLs for redirects and so all197        redirect exceptions raised by Werkzeug will contain the full canonical198        URL.199 200        If no path_info is passed to :meth:`match` it will use the default path201        info passed to bind.  While this doesn't really make sense for202        manual bind calls, it's useful if you bind a map to a WSGI203        environment which already contains the path info.204 205        `subdomain` will default to the `default_subdomain` for this map if206        no defined. If there is no `default_subdomain` you cannot use the207        subdomain feature.208 209        .. versionchanged:: 1.0210            If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules211            will match.212 213        .. versionchanged:: 0.15214            ``path_info`` defaults to ``'/'`` if ``None``.215 216        .. versionchanged:: 0.8217            ``query_args`` can be a string.218 219        .. versionchanged:: 0.7220            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_subdomain228        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 e240 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 it259        will fetch the information from that dictionary.  Note that because of260        limitations in the protocol there is no way to get the current261        subdomain and real `server_name` from the environment.  If you don't262        provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or263        `HTTP_HOST` if provided) as used `server_name` with disabled subdomain264        feature.265 266        If `subdomain` is `None` but an environment and a server name is267        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 calculated270        subdomain will be ``'staging.dev'``.271 272        If the object passed as environ has an environ attribute, the value of273        this attribute is used instead.  This allows you to pass request274        objects.  Additionally `PATH_INFO` added as a default of the275        :class:`MapAdapter` so that you don't have to pass the path info to276        the match method.277 278        .. versionchanged:: 1.0.0279            If the passed server name specifies port 443, it will match280            if the incoming scheme is ``https`` without a port.281 282        .. versionchanged:: 1.0.0283            A warning is shown when the passed server name does not284            match the incoming WSGI server name.285 286        .. versionchanged:: 0.8287           This will no longer raise a ValueError when an unexpected server288           name was passed.289 290        .. versionchanged:: 0.5291            previously this method accepted a bogus `calculate_subdomain`292            parameter that did not have any effect.  It was removed because293            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_name312        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 was328                # accessed directly by IP address under some situations.329                # Instead of raising an exception like in Werkzeug 0.7 or330                # earlier we go by an invalid subdomain which will result331                # 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 None346 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 rules363        in the correct order after things changed.364        """365        if not self._remap:366            return367 368        with self._remap_lock:369            if not self._remap:370                return371 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 = False376 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 does384    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 = map399        self.server_name = server_name400 401        if not script_name.endswith("/"):402            script_name += "/"403 404        self.script_name = script_name405        self.subdomain = subdomain406        self.url_scheme = url_scheme407        self.path_info = path_info408        self.default_method = default_method409        self.query_args = query_args410        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 with420        the endpoint and a dict with the values for the view.  It should421        look up the view function, call it, and return a response object422        or WSGI application.  http exceptions are not caught by default423        so that applications can display nicer error messages by just424        catching them by hand.  If you want to stick with the default425        error messages you can pass it ``catch_http_exceptions=True`` and426        it will catch the http exceptions.427 428        Here a small example for the dispatch usage::429 430            from werkzeug.wrappers import Request, Response431            from werkzeug.wsgi import responder432            from werkzeug.routing import Map, Rule433 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            @responder441            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, so448        use :class:`Response.force_type` to get a response object.449 450        :param view_func: a function that is called with the endpoint as451                          first argument and the value dict as second.  Has452                          to dispatch to the actual view function with this453                          information.  (see above)454        :param path_info: the path info to use for matching.  Overrides the455                          path info specified on binding.456        :param method: the HTTP method used for matching.  Overrides the457                       method specified on binding.458        :param catch_http_exceptions: set to `True` to catch any of the459                                      werkzeug :class:`HTTPException`\\s.460        """461        try:462            try:463                endpoint, args = self.match(path_info, method)464            except RequestRedirect as e:465                return e466            return view_func(endpoint, args)467        except HTTPException as e:468            if catch_http_exceptions:469                return e470            raise471 472    @t.overload473    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.overload483    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 current501        path info as well as the method (which defaults to `GET`).  The502        following things can then happen:503 504        - you receive a `NotFound` exception that indicates that no URL is505          matching.  A `NotFound` exception is also a WSGI application you506          can call to get a default page not found page (happens to be the507          same object as `werkzeug.exceptions.NotFound`)508 509        - you receive a `MethodNotAllowed` exception that indicates that there510          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 request515          Werkzeug requests from your WSGI application.  This is for example the516          case if you request ``/foo`` although the correct URL is ``/foo/``517          You can use the `RequestRedirect` instance as response-like object518          similar to all other subclasses of `HTTPException`.519 520        - you receive a ``WebsocketMismatch`` exception if the only521          match is a WebSocket rule but the bind is an HTTP request, or522          if the match is an HTTP rule but the bind is a WebSocket523          request.524 525        - you get a tuple in the form ``(endpoint, arguments)`` if there is526          a match (unless `return_rule` is True, in which case you get a tuple527          in the form ``(rule, arguments)``)528 529        If the path info is not passed to the match method the default path530        info of the map is used (defaults to the root URL if not defined531        explicitly).532 533        All of the exceptions raised are subclasses of `HTTPException` so they534        can be used as WSGI responses. They will all render generic error or535        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 Found560 561        :param path_info: the path info to use for matching.  Overrides the562                          path info specified on binding.563        :param method: the HTTP method used for matching.  Overrides the564                       method specified on binding.565        :param return_rule: return the rule that matched instead of just the566                            endpoint (defaults to `False`).567        :param query_args: optional query arguments that are used for568                           automatic redirects as string or dictionary.  It's569                           currently not possible to use the query arguments570                           for URL matching.571        :param websocket: Match WebSocket instead of HTTP requests. A572            websocket request has a ``ws`` or ``wss``573            :attr:`url_scheme`. This overrides that detection.574 575        .. versionadded:: 1.0576            Added ``websocket``.577 578        .. versionchanged:: 0.8579            ``query_args`` can be a string.580 581        .. versionadded:: 0.7582            Added ``query_args``.583 584        .. versionadded:: 0.6585            Added ``return_rule``.586        """587        self.map.update()588        if path_info is None:589            path_info = self.path_info590        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.websocket596 597        domain_part = self.server_name598 599        if not self.map.host_matching and self.subdomain is not None:600            domain_part = self.subdomain601 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-string608            new_path = quote(e.path_info, safe="!$&'()*+,/:;=@")609            raise RequestRedirect(610                self.make_redirect_url(new_path, query_args)611            ) from None612        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 None622        except NoMatch as e:623            if e.have_match_for:624                raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None625 626            if e.websocket_mismatch:627                raise WebsocketMismatch() from None628 629            raise NotFound() from None630        else:631            rule, rv = result632 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_name653 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, rv663            else:664                return rule.endpoint, rv665 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 the671                          path info specified on binding.672        :param method: the HTTP method used for matching.  Overrides the673                       method specified on binding.674        """675        try:676            self.match(path_info, method)677        except RequestRedirect:678            pass679        except HTTPException:680            return False681        return True682 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.7687        """688        try:689            self.match(path_info, method="--")690        except MethodNotAllowed as e:691            return e.valid_methods  # type: ignore692        except HTTPException:693            pass694        return []695 696    def get_host(self, domain_part: str | None) -> str:697        """Figures out the full host name for the given domain part.  The698        domain part is a subdomain in case host matching is disabled or699        a full host name.700        """701        if self.map.host_matching:702            if domain_part is None:703                return self.server_name704 705            return domain_part706 707        if domain_part is None:708            subdomain = self.subdomain709        else:710            subdomain = domain_part711 712        if subdomain:713            return f"{subdomain}.{self.server_name}"714        else:715            return self.server_name716 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_defaults730        for r in self.map._rules_by_endpoint[rule.endpoint]:731            # every rule that comes after this one, including ourself732            # has a lower priority for the defaults.  We order the ones733            # with the highest priority up for building.734            if r is rule:735                break736            if r.provides_defaults_for(rule) and r.suitable_for(values, method):737                values.update(r.defaults)  # type: ignore738                domain_part, path = r.build(values)  # type: ignore739                return self.make_redirect_url(path, query_args, domain_part=domain_part)740        return None741 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_args746 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_args759 760        if query_args:761            query_str = self.encode_query_args(query_args)762        else:763            query_str = None764 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=True781        )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 url786 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 the795        rule that accepts this endpoint, values and method.796 797        :internal:798        """799        # in case the method is none, try with the default method first800        if method is None:801            rv = self._partial_build(802                endpoint, values, self.default_method, append_unknown803            )804            if rv is not None:805                return rv806 807        # Default method did not match or a specific method is passed.808        # Check all for first match with matching host. If no matching809        # host is found, go with first result.810        first_match = None811 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 rv821                        elif first_match is None:822                            first_match = rv823                    else:824                        return rv825 826        return first_match827 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        """Build a URL by filling in any variable slots in the endpoint's rule838        with the provided values.839 840        This produces relative URLs (path only, no host) by default, which is841        appropriate for most use cases. When using subdomain or host matching,842        the URL will be absolute if the endpoint is not on the same host as the843        current request. If you need an absolute URL no matter what, such as844        when adding a link to an email, pass ``force_external=True``.845 846        Characters that are not allowed directly in URLs will be percent-encoded847        using UTF-8.848 849        Additional values that don't match any rule parts are added to the query850        (``?key=value``) part of the URL.851 852        If a rule is not found, an :exc:`.BuildError` is raised. Converters may853        also raise exceptions when preparing a value.854 855        Unlike matching, building does not perform comprehensive validation of856        the values, it's assumed that they are trusted and correct. It's also857        possible to build a URL for one endpoint that will be matched by858        another, or will not match. This is very unlikely to happen or to matter859        in most applications, especially with testing. If you need a860        stronger guarantee, you can use a helper to verify that861        ``match(build(endpoint)) == endpoint``.862 863        :param endpoint: The endpoint for the rule to build.864        :param values: Values for the variable parts of the rule, which will be865            converted using the part's converter's866            :meth:`~.BaseConverter.to_url` method. Keys that don't match867            variable parts will be converted to a query string unless868            ``append_unknown`` is disabled.869        :param method: Further match the rule to build by this HTTP method, for870            when there are multiple rules for the same endpoint that vary based871            on method.872        :param force_external: Always produce an absolute URL, for external use.873            If ``scheme`` is not set, this produces a protocol-relative URL.874        :param append_unknown: Convert values that don't match any rule parts875            to a query string. Disable to ignore such values.876        :param url_scheme: Scheme to use for absolute URLs. Defaults to877            :attr:`url_scheme`.878 879        .. versionchanged:: 2.0880            Added the ``url_scheme`` parameter.881 882        .. versionadded:: 0.6883           Added the ``append_unknown`` parameter.884        """885        self.map.update()886 887        if values:888            if isinstance(values, MultiDict):889                values = {890                    k: (v[0] if len(v) == 1 else v)891                    for k, v in dict.items(values)892                    if len(v) != 0893                }894            else:  # plain dict895                values = {k: v for k, v in values.items() if v is not None}896        else:897            values = {}898 899        rv = self._partial_build(endpoint, values, method, append_unknown)900        if rv is None:901            raise BuildError(endpoint, values, method, self)902 903        domain_part, path, websocket = rv904        host = self.get_host(domain_part)905 906        if url_scheme is None:907            url_scheme = self.url_scheme908 909        # Always build WebSocket routes with the scheme (browsers910        # require full URLs). If bound to a WebSocket, ensure that HTTP911        # routes are built with an HTTP scheme.912        secure = url_scheme in {"https", "wss"}913 914        if websocket:915            force_external = True916            url_scheme = "wss" if secure else "ws"917        elif url_scheme:918            url_scheme = "https" if secure else "http"919 920        # shortcut this.921        if not force_external and (922            (self.map.host_matching and host == self.server_name)923            or (not self.map.host_matching and domain_part == self.subdomain)924        ):925            return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}"926 927        scheme = f"{url_scheme}:" if url_scheme else ""928        return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}"929 
codekingpro/portable-devtools · Team Ai