Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
routing.py729 linesDownload Raw Back to tornado
1# Copyright 2015 The Tornado Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15"""Flexible routing implementation.
16
17Tornado routes HTTP requests to appropriate handlers using `Router`
18class implementations. The `tornado.web.Application` class is a
19`Router` implementation and may be used directly, or the classes in
20this module may be used for additional flexibility. The `RuleRouter`
21class can match on more criteria than `.Application`, or the `Router`
22interface can be subclassed for maximum customization.
23
24`Router` interface extends `~.httputil.HTTPServerConnectionDelegate`
25to provide additional routing capabilities. This also means that any
26`Router` implementation can be used directly as a ``request_callback``
27for `~.httpserver.HTTPServer` constructor.
28
29`Router` subclass must implement a ``find_handler`` method to provide
30a suitable `~.httputil.HTTPMessageDelegate` instance to handle the
31request:
32
33.. code-block:: python
34
35    class CustomRouter(Router):
36        def find_handler(self, request, **kwargs):
37            # some routing logic providing a suitable HTTPMessageDelegate instance
38            return MessageDelegate(request.connection)
39
40    class MessageDelegate(HTTPMessageDelegate):
41        def __init__(self, connection):
42            self.connection = connection
43
44        def finish(self):
45            self.connection.write_headers(
46                ResponseStartLine("HTTP/1.1", 200, "OK"),
47                HTTPHeaders({"Content-Length": "2"}),
48                b"OK")
49            self.connection.finish()
50
51    router = CustomRouter()
52    server = HTTPServer(router)
53
54The main responsibility of `Router` implementation is to provide a
55mapping from a request to `~.httputil.HTTPMessageDelegate` instance
56that will handle this request. In the example above we can see that
57routing is possible even without instantiating an `~.web.Application`.
58
59For routing to `~.web.RequestHandler` implementations we need an
60`~.web.Application` instance. `~.web.Application.get_handler_delegate`
61provides a convenient way to create `~.httputil.HTTPMessageDelegate`
62for a given request and `~.web.RequestHandler`.
63
64Here is a simple example of how we can we route to
65`~.web.RequestHandler` subclasses by HTTP method:
66
67.. code-block:: python
68
69    resources = {}
70
71    class GetResource(RequestHandler):
72        def get(self, path):
73            if path not in resources:
74                raise HTTPError(404)
75
76            self.finish(resources[path])
77
78    class PostResource(RequestHandler):
79        def post(self, path):
80            resources[path] = self.request.body
81
82    class HTTPMethodRouter(Router):
83        def __init__(self, app):
84            self.app = app
85
86        def find_handler(self, request, **kwargs):
87            handler = GetResource if request.method == "GET" else PostResource
88            return self.app.get_handler_delegate(request, handler, path_args=[request.path])
89
90    router = HTTPMethodRouter(Application())
91    server = HTTPServer(router)
92
93`ReversibleRouter` interface adds the ability to distinguish between
94the routes and reverse them to the original urls using route's name
95and additional arguments. `~.web.Application` is itself an
96implementation of `ReversibleRouter` class.
97
98`RuleRouter` and `ReversibleRuleRouter` are implementations of
99`Router` and `ReversibleRouter` interfaces and can be used for
100creating rule-based routing configurations.
101
102Rules are instances of `Rule` class. They contain a `Matcher`, which
103provides the logic for determining whether the rule is a match for a
104particular request and a target, which can be one of the following.
105
1061) An instance of `~.httputil.HTTPServerConnectionDelegate`:
107
108.. code-block:: python
109
110    router = RuleRouter([
111        Rule(PathMatches("/handler"), ConnectionDelegate()),
112        # ... more rules
113    ])
114
115    class ConnectionDelegate(HTTPServerConnectionDelegate):
116        def start_request(self, server_conn, request_conn):
117            return MessageDelegate(request_conn)
118
1192) A callable accepting a single argument of `~.httputil.HTTPServerRequest` type:
120
121.. code-block:: python
122
123    router = RuleRouter([
124        Rule(PathMatches("/callable"), request_callable)
125    ])
126
127    def request_callable(request):
128        request.write(b"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nOK")
129        request.finish()
130
1313) Another `Router` instance:
132
133.. code-block:: python
134
135    router = RuleRouter([
136        Rule(PathMatches("/router.*"), CustomRouter())
137    ])
138
139Of course a nested `RuleRouter` or a `~.web.Application` is allowed:
140
141.. code-block:: python
142
143    router = RuleRouter([
144        Rule(HostMatches("example.com"), RuleRouter([
145            Rule(PathMatches("/app1/.*"), Application([(r"/app1/handler", Handler)])),
146        ]))
147    ])
148
149    server = HTTPServer(router)
150
151In the example below `RuleRouter` is used to route between applications:
152
153.. code-block:: python
154
155    app1 = Application([
156        (r"/app1/handler", Handler1),
157        # other handlers ...
158    ])
159
160    app2 = Application([
161        (r"/app2/handler", Handler2),
162        # other handlers ...
163    ])
164
165    router = RuleRouter([
166        Rule(PathMatches("/app1.*"), app1),
167        Rule(PathMatches("/app2.*"), app2)
168    ])
169
170    server = HTTPServer(router)
171
172For more information on application-level routing see docs for `~.web.Application`.
173
174.. versionadded:: 4.5
175
176"""
177
178import re
179from functools import partial
180
181from tornado import httputil
182from tornado.httpserver import _CallableAdapter
183from tornado.escape import url_escape, url_unescape, utf8
184from tornado.log import app_log
185from tornado.util import basestring_type, import_object, re_unescape, unicode_type
186
187from typing import (
188    Any,
189    Union,
190    Optional,
191    Awaitable,
192    List,
193    Dict,
194    Pattern,
195    Tuple,
196    overload,
197    Sequence,
198)
199
200
201class Router(httputil.HTTPServerConnectionDelegate):
202    """Abstract router interface."""
203
204    def find_handler(
205        self, request: httputil.HTTPServerRequest, **kwargs: Any
206    ) -> Optional[httputil.HTTPMessageDelegate]:
207        """Must be implemented to return an appropriate instance of `~.httputil.HTTPMessageDelegate`
208        that can serve the request.
209        Routing implementations may pass additional kwargs to extend the routing logic.
210
211        :arg httputil.HTTPServerRequest request: current HTTP request.
212        :arg kwargs: additional keyword arguments passed by routing implementation.
213        :returns: an instance of `~.httputil.HTTPMessageDelegate` that will be used to
214            process the request.
215        """
216        raise NotImplementedError()
217
218    def start_request(
219        self, server_conn: object, request_conn: httputil.HTTPConnection
220    ) -> httputil.HTTPMessageDelegate:
221        return _RoutingDelegate(self, server_conn, request_conn)
222
223
224class ReversibleRouter(Router):
225    """Abstract router interface for routers that can handle named routes
226    and support reversing them to original urls.
227    """
228
229    def reverse_url(self, name: str, *args: Any) -> Optional[str]:
230        """Returns url string for a given route name and arguments
231        or ``None`` if no match is found.
232
233        :arg str name: route name.
234        :arg args: url parameters.
235        :returns: parametrized url string for a given route name (or ``None``).
236        """
237        raise NotImplementedError()
238
239
240class _RoutingDelegate(httputil.HTTPMessageDelegate):
241    def __init__(
242        self, router: Router, server_conn: object, request_conn: httputil.HTTPConnection
243    ) -> None:
244        self.server_conn = server_conn
245        self.request_conn = request_conn
246        self.delegate = None  # type: Optional[httputil.HTTPMessageDelegate]
247        self.router = router  # type: Router
248
249    def headers_received(
250        self,
251        start_line: Union[httputil.RequestStartLine, httputil.ResponseStartLine],
252        headers: httputil.HTTPHeaders,
253    ) -> Optional[Awaitable[None]]:
254        assert isinstance(start_line, httputil.RequestStartLine)
255        request = httputil.HTTPServerRequest(
256            connection=self.request_conn,
257            server_connection=self.server_conn,
258            start_line=start_line,
259            headers=headers,
260        )
261
262        self.delegate = self.router.find_handler(request)
263        if self.delegate is None:
264            app_log.debug(
265                "Delegate for %s %s request not found",
266                start_line.method,
267                start_line.path,
268            )
269            self.delegate = _DefaultMessageDelegate(self.request_conn)
270
271        return self.delegate.headers_received(start_line, headers)
272
273    def data_received(self, chunk: bytes) -> Optional[Awaitable[None]]:
274        assert self.delegate is not None
275        return self.delegate.data_received(chunk)
276
277    def finish(self) -> None:
278        assert self.delegate is not None
279        self.delegate.finish()
280
281    def on_connection_close(self) -> None:
282        if self.delegate is not None:
283            self.delegate.on_connection_close()
284
285
286class _DefaultMessageDelegate(httputil.HTTPMessageDelegate):
287    def __init__(self, connection: httputil.HTTPConnection) -> None:
288        self.connection = connection
289
290    def finish(self) -> None:
291        self.connection.write_headers(
292            httputil.ResponseStartLine("HTTP/1.1", 404, "Not Found"),
293            httputil.HTTPHeaders(),
294        )
295        self.connection.finish()
296
297
298# _RuleList can either contain pre-constructed Rules or a sequence of
299# arguments to be passed to the Rule constructor.
300_RuleList = Sequence[
301    Union[
302        "Rule",
303        List[Any],  # Can't do detailed typechecking of lists.
304        Tuple[Union[str, "Matcher"], Any],
305        Tuple[Union[str, "Matcher"], Any, Dict[str, Any]],
306        Tuple[Union[str, "Matcher"], Any, Dict[str, Any], str],
307    ]
308]
309
310
311class RuleRouter(Router):
312    """Rule-based router implementation."""
313
314    def __init__(self, rules: Optional[_RuleList] = None) -> None:
315        """Constructs a router from an ordered list of rules::
316
317            RuleRouter([
318                Rule(PathMatches("/handler"), Target),
319                # ... more rules
320            ])
321
322        You can also omit explicit `Rule` constructor and use tuples of arguments::
323
324            RuleRouter([
325                (PathMatches("/handler"), Target),
326            ])
327
328        `PathMatches` is a default matcher, so the example above can be simplified::
329
330            RuleRouter([
331                ("/handler", Target),
332            ])
333
334        In the examples above, ``Target`` can be a nested `Router` instance, an instance of
335        `~.httputil.HTTPServerConnectionDelegate` or an old-style callable,
336        accepting a request argument.
337
338        :arg rules: a list of `Rule` instances or tuples of `Rule`
339            constructor arguments.
340        """
341        self.rules = []  # type: List[Rule]
342        if rules:
343            self.add_rules(rules)
344
345    def add_rules(self, rules: _RuleList) -> None:
346        """Appends new rules to the router.
347
348        :arg rules: a list of Rule instances (or tuples of arguments, which are
349            passed to Rule constructor).
350        """
351        for rule in rules:
352            if isinstance(rule, (tuple, list)):
353                assert len(rule) in (2, 3, 4)
354                if isinstance(rule[0], basestring_type):
355                    rule = Rule(PathMatches(rule[0]), *rule[1:])
356                else:
357                    rule = Rule(*rule)
358
359            self.rules.append(self.process_rule(rule))
360
361    def process_rule(self, rule: "Rule") -> "Rule":
362        """Override this method for additional preprocessing of each rule.
363
364        :arg Rule rule: a rule to be processed.
365        :returns: the same or modified Rule instance.
366        """
367        return rule
368
369    def find_handler(
370        self, request: httputil.HTTPServerRequest, **kwargs: Any
371    ) -> Optional[httputil.HTTPMessageDelegate]:
372        for rule in self.rules:
373            target_params = rule.matcher.match(request)
374            if target_params is not None:
375                if rule.target_kwargs:
376                    target_params["target_kwargs"] = rule.target_kwargs
377
378                delegate = self.get_target_delegate(
379                    rule.target, request, **target_params
380                )
381
382                if delegate is not None:
383                    return delegate
384
385        return None
386
387    def get_target_delegate(
388        self, target: Any, request: httputil.HTTPServerRequest, **target_params: Any
389    ) -> Optional[httputil.HTTPMessageDelegate]:
390        """Returns an instance of `~.httputil.HTTPMessageDelegate` for a
391        Rule's target. This method is called by `~.find_handler` and can be
392        extended to provide additional target types.
393
394        :arg target: a Rule's target.
395        :arg httputil.HTTPServerRequest request: current request.
396        :arg target_params: additional parameters that can be useful
397            for `~.httputil.HTTPMessageDelegate` creation.
398        """
399        if isinstance(target, Router):
400            return target.find_handler(request, **target_params)
401
402        elif isinstance(target, httputil.HTTPServerConnectionDelegate):
403            assert request.connection is not None
404            return target.start_request(request.server_connection, request.connection)
405
406        elif callable(target):
407            assert request.connection is not None
408            return _CallableAdapter(
409                partial(target, **target_params), request.connection
410            )
411
412        return None
413
414
415class ReversibleRuleRouter(ReversibleRouter, RuleRouter):
416    """A rule-based router that implements ``reverse_url`` method.
417
418    Each rule added to this router may have a ``name`` attribute that can be
419    used to reconstruct an original uri. The actual reconstruction takes place
420    in a rule's matcher (see `Matcher.reverse`).
421    """
422
423    def __init__(self, rules: Optional[_RuleList] = None) -> None:
424        self.named_rules = {}  # type: Dict[str, Any]
425        super().__init__(rules)
426
427    def process_rule(self, rule: "Rule") -> "Rule":
428        rule = super().process_rule(rule)
429
430        if rule.name:
431            if rule.name in self.named_rules:
432                app_log.warning(
433                    "Multiple handlers named %s; replacing previous value", rule.name
434                )
435            self.named_rules[rule.name] = rule
436
437        return rule
438
439    def reverse_url(self, name: str, *args: Any) -> Optional[str]:
440        if name in self.named_rules:
441            return self.named_rules[name].matcher.reverse(*args)
442
443        for rule in self.rules:
444            if isinstance(rule.target, ReversibleRouter):
445                reversed_url = rule.target.reverse_url(name, *args)
446                if reversed_url is not None:
447                    return reversed_url
448
449        return None
450
451
452class Rule:
453    """A routing rule."""
454
455    def __init__(
456        self,
457        matcher: "Matcher",
458        target: Any,
459        target_kwargs: Optional[Dict[str, Any]] = None,
460        name: Optional[str] = None,
461    ) -> None:
462        """Constructs a Rule instance.
463
464        :arg Matcher matcher: a `Matcher` instance used for determining
465            whether the rule should be considered a match for a specific
466            request.
467        :arg target: a Rule's target (typically a ``RequestHandler`` or
468            `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`,
469            depending on routing implementation).
470        :arg dict target_kwargs: a dict of parameters that can be useful
471            at the moment of target instantiation (for example, ``status_code``
472            for a ``RequestHandler`` subclass). They end up in
473            ``target_params['target_kwargs']`` of `RuleRouter.get_target_delegate`
474            method.
475        :arg str name: the name of the rule that can be used to find it
476            in `ReversibleRouter.reverse_url` implementation.
477        """
478        if isinstance(target, str):
479            # import the Module and instantiate the class
480            # Must be a fully qualified name (module.ClassName)
481            target = import_object(target)
482
483        self.matcher = matcher  # type: Matcher
484        self.target = target
485        self.target_kwargs = target_kwargs if target_kwargs else {}
486        self.name = name
487
488    def reverse(self, *args: Any) -> Optional[str]:
489        return self.matcher.reverse(*args)
490
491    def __repr__(self) -> str:
492        return "{}({!r}, {}, kwargs={!r}, name={!r})".format(
493            self.__class__.__name__,
494            self.matcher,
495            self.target,
496            self.target_kwargs,
497            self.name,
498        )
499
500
501class Matcher:
502    """Represents a matcher for request features."""
503
504    def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
505        """Matches current instance against the request.
506
507        :arg httputil.HTTPServerRequest request: current HTTP request
508        :returns: a dict of parameters to be passed to the target handler
509            (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs``
510            can be passed for proper `~.web.RequestHandler` instantiation).
511            An empty dict is a valid (and common) return value to indicate a match
512            when the argument-passing features are not used.
513            ``None`` must be returned to indicate that there is no match."""
514        raise NotImplementedError()
515
516    def reverse(self, *args: Any) -> Optional[str]:
517        """Reconstructs full url from matcher instance and additional arguments."""
518        return None
519
520
521class AnyMatches(Matcher):
522    """Matches any request."""
523
524    def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
525        return {}
526
527
528class HostMatches(Matcher):
529    """Matches requests from hosts specified by ``host_pattern`` regex."""
530
531    def __init__(self, host_pattern: Union[str, Pattern]) -> None:
532        if isinstance(host_pattern, basestring_type):
533            if not host_pattern.endswith("$"):
534                host_pattern += "$"
535            self.host_pattern = re.compile(host_pattern)
536        else:
537            self.host_pattern = host_pattern
538
539    def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
540        if self.host_pattern.match(request.host_name):
541            return {}
542
543        return None
544
545
546class DefaultHostMatches(Matcher):
547    """Matches requests from host that is equal to application's default_host.
548    Always returns no match if ``X-Real-Ip`` header is present.
549    """
550
551    def __init__(self, application: Any, host_pattern: Pattern) -> None:
552        self.application = application
553        self.host_pattern = host_pattern
554
555    def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
556        # Look for default host if not behind load balancer (for debugging)
557        if "X-Real-Ip" not in request.headers:
558            if self.host_pattern.match(self.application.default_host):
559                return {}
560        return None
561
562
563class PathMatches(Matcher):
564    """Matches requests with paths specified by ``path_pattern`` regex."""
565
566    def __init__(self, path_pattern: Union[str, Pattern]) -> None:
567        if isinstance(path_pattern, basestring_type):
568            if not path_pattern.endswith("$"):
569                path_pattern += "$"
570            self.regex = re.compile(path_pattern)
571        else:
572            self.regex = path_pattern
573
574        assert len(self.regex.groupindex) in (0, self.regex.groups), (
575            "groups in url regexes must either be all named or all "
576            "positional: %r" % self.regex.pattern
577        )
578
579        self._path, self._group_count = self._find_groups()
580
581    def match(self, request: httputil.HTTPServerRequest) -> Optional[Dict[str, Any]]:
582        match = self.regex.match(request.path)
583        if match is None:
584            return None
585        if not self.regex.groups:
586            return {}
587
588        path_args = []  # type: List[bytes]
589        path_kwargs = {}  # type: Dict[str, bytes]
590
591        # Pass matched groups to the handler.  Since
592        # match.groups() includes both named and
593        # unnamed groups, we want to use either groups
594        # or groupdict but not both.
595        if self.regex.groupindex:
596            path_kwargs = {
597                str(k): _unquote_or_none(v) for (k, v) in match.groupdict().items()
598            }
599        else:
600            path_args = [_unquote_or_none(s) for s in match.groups()]
601
602        return dict(path_args=path_args, path_kwargs=path_kwargs)
603
604    def reverse(self, *args: Any) -> Optional[str]:
605        if self._path is None:
606            raise ValueError("Cannot reverse url regex " + self.regex.pattern)
607        assert len(args) == self._group_count, (
608            "required number of arguments " "not found"
609        )
610        if not len(args):
611            return self._path
612        converted_args = []
613        for a in args:
614            if not isinstance(a, (unicode_type, bytes)):
615                a = str(a)
616            converted_args.append(url_escape(utf8(a), plus=False))
617        return self._path % tuple(converted_args)
618
619    def _find_groups(self) -> Tuple[Optional[str], Optional[int]]:
620        """Returns a tuple (reverse string, group count) for a url.
621
622        For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
623        would return ('/%s/%s/', 2).
624        """
625        pattern = self.regex.pattern
626        if pattern.startswith("^"):
627            pattern = pattern[1:]
628        if pattern.endswith("$"):
629            pattern = pattern[:-1]
630
631        if self.regex.groups != pattern.count("("):
632            # The pattern is too complicated for our simplistic matching,
633            # so we can't support reversing it.
634            return None, None
635
636        pieces = []
637        for fragment in pattern.split("("):
638            if ")" in fragment:
639                paren_loc = fragment.index(")")
640                if paren_loc >= 0:
641                    try:
642                        unescaped_fragment = re_unescape(fragment[paren_loc + 1 :])
643                    except ValueError:
644                        # If we can't unescape part of it, we can't
645                        # reverse this url.
646                        return (None, None)
647                    pieces.append("%s" + unescaped_fragment)
648            else:
649                try:
650                    unescaped_fragment = re_unescape(fragment)
651                except ValueError:
652                    # If we can't unescape part of it, we can't
653                    # reverse this url.
654                    return (None, None)
655                pieces.append(unescaped_fragment)
656
657        return "".join(pieces), self.regex.groups
658
659
660class URLSpec(Rule):
661    """Specifies mappings between URLs and handlers.
662
663    .. versionchanged: 4.5
664       `URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for
665       backwards compatibility.
666    """
667
668    def __init__(
669        self,
670        pattern: Union[str, Pattern],
671        handler: Any,
672        kwargs: Optional[Dict[str, Any]] = None,
673        name: Optional[str] = None,
674    ) -> None:
675        """Parameters:
676
677        * ``pattern``: Regular expression to be matched. Any capturing
678          groups in the regex will be passed in to the handler's
679          get/post/etc methods as arguments (by keyword if named, by
680          position if unnamed. Named and unnamed capturing groups
681          may not be mixed in the same rule).
682
683        * ``handler``: `~.web.RequestHandler` subclass to be invoked.
684
685        * ``kwargs`` (optional): A dictionary of additional arguments
686          to be passed to the handler's constructor.
687
688        * ``name`` (optional): A name for this handler.  Used by
689          `~.web.Application.reverse_url`.
690
691        """
692        matcher = PathMatches(pattern)
693        super().__init__(matcher, handler, kwargs, name)
694
695        self.regex = matcher.regex
696        self.handler_class = self.target
697        self.kwargs = kwargs
698
699    def __repr__(self) -> str:
700        return "{}({!r}, {}, kwargs={!r}, name={!r})".format(
701            self.__class__.__name__,
702            self.regex.pattern,
703            self.handler_class,
704            self.kwargs,
705            self.name,
706        )
707
708
709@overload
710def _unquote_or_none(s: str) -> bytes:
711    pass
712
713
714@overload  # noqa: F811
715def _unquote_or_none(s: None) -> None:
716    pass
717
718
719def _unquote_or_none(s: Optional[str]) -> Optional[bytes]:  # noqa: F811
720    """None-safe wrapper around url_unescape to handle unmatched optional
721    groups correctly.
722
723    Note that args are passed as bytes so the handler can decide what
724    encoding to use.
725    """
726    if s is None:
727        return s
728    return url_unescape(s, encoding=None, plus=False)
729 
codekingpro/portable-devtools · Team Ai