]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
improve routing module docs and tests 1916/head
authorAndrew Sumin <sumin.andrew@gmail.com>
Sat, 17 Dec 2016 16:56:17 +0000 (19:56 +0300)
committerAndrey Sumin <an.sumin@hh.ru>
Mon, 19 Dec 2016 14:46:04 +0000 (17:46 +0300)
tornado/routing.py
tornado/test/routing_test.py
tornado/web.py

index 15baeb489bb21f49d456879d323d51fef5954340..71c63b3db588b39a3b2a47df83ce5f1b95b44fb1 100644 (file)
 
 Tornado routes HTTP requests to appropriate handlers using `Router` class implementations.
 
-Any `Router` implementation can be used directly as a ``request_callback`` for
-`~.httpserver.HTTPServer` (this is possible because `Router` implements
-`~.httputil.HTTPServerConnectionDelegate`):
+`Router` interface extends `~.httputil.HTTPServerConnectionDelegate` to provide additional
+routing capabilities. This also means that any `Router` implementation can be used directly
+as a ``request_callback`` for `~.httpserver.HTTPServer` constructor.
+
+`Router` subclass must implement a ``find_handler`` method to provide a suitable
+`~.httputil.HTTPMessageDelegate` instance to handle the request:
 
 .. code-block:: python
 
     class CustomRouter(Router):
         def find_handler(self, request, **kwargs):
-            # some routing logic providing a suitable HTTPMessageDelegate implementation
-            return HTTPMessageDelegateImplementation()
+            # some routing logic providing a suitable HTTPMessageDelegate instance
+            return MessageDelegate(request.connection)
+
+    class MessageDelegate(HTTPMessageDelegate):
+        def __init__(self, connection):
+            self.connection = connection
+
+        def finish(self):
+            self.connection.write_headers(
+                ResponseStartLine("HTTP/1.1", 200, "OK"),
+                HTTPHeaders({"Content-Length": "2"}),
+                b"OK")
+            self.connection.finish()
 
     router = CustomRouter()
     server = HTTPServer(router)
 
-`Router.find_handler` is the only method that you must implement to provide routing
-logic in its simplest case. This method must return an instance of
-`~.httputil.HTTPMessageDelegate` that will be used to process the request.
+The main responsibility of `Router` implementation is to provide a mapping from a request
+to `~.httputil.HTTPMessageDelegate` instance that will handle this request. In the example above
+we can see that routing is possible even without instantiating an `~.web.Application`.
+
+For routing to `~.web.RequestHandler` implementations we need an `~.web.Application` instance.
+`~.web.Application.get_handler_delegate` provides a convenient way to create
+`~.httputil.HTTPMessageDelegate` for a given request and `~.web.RequestHandler`.
+
+Here is a simple example of how we can we route to `~.web.RequestHandler` subclasses
+by HTTP method:
+
+.. code-block:: python
+
+    resources = {}
+
+    class GetResource(RequestHandler):
+        def get(self, path):
+            if path not in resources:
+                raise HTTPError(404)
+
+            self.finish(resources[path])
+
+    class PostResource(RequestHandler):
+        def post(self, path):
+            resources[path] = self.request.body
+
+    class HTTPMethodRouter(Router):
+        def __init__(self, app):
+            self.app = app
+
+        def find_handler(self, request, **kwargs):
+            handler = GetResource if request.method == "GET" else PostResource
+            return self.app.get_handler_delegate(request, handler, path_args=[request.path])
+
+    router = HTTPMethodRouter(Application())
+    server = HTTPServer(router)
 
 `ReversibleRouter` interface adds the ability to distinguish between the routes and
 reverse them to the original urls using route's name and additional arguments.
 `~.web.Application` is itself an implementation of `ReversibleRouter` class.
 
-`RuleRouter` and `ReversibleRuleRouter` provide an interface for creating rule-based
-routing configurations. For example, `RuleRouter` can be used to route between applications:
+`RuleRouter` and `ReversibleRuleRouter` are implementations of `Router` and `ReversibleRouter`
+interfaces and can be used for creating rule-based routing configurations.
 
-.. code-block:: python
+Rules are instances of `Rule` class. They contain a `Matcher`, which provides the logic for
+determining whether the rule is a match for a particular request and a target, which can be
+one of the following.
 
-    app1 = Application([
-        (r"/app1/handler1", Handler1),
-        # other handlers ...
-    ])
+1) An instance of `~.httputil.HTTPServerConnectionDelegate`:
 
-    app2 = Application([
-        (r"/app2/handler1", Handler1),
-        # other handlers ...
-    ])
+.. code-block:: python
 
     router = RuleRouter([
-        Rule(PathMatches("/app1.*"), app1),
-        Rule(PathMatches("/app2.*"), app2)
+        Rule(PathMatches("/handler"), ConnectionDelegate()),
+        # ... more rules
     ])
 
-    server = HTTPServer(router)
+    class ConnectionDelegate(HTTPServerConnectionDelegate):
+        def start_request(self, server_conn, request_conn):
+            return MessageDelegate(request_conn)
 
-Implementations of `~.httputil.HTTPServerConnectionDelegate` and old-style callables can also be used as
-rule targets:
+2) A callable accepting a single argument of `~.httputil.HTTPServerRequest` type:
 
 .. code-block:: python
 
     router = RuleRouter([
-        Rule(PathMatches("/callable"), request_callable),  # def request_callable(request): ...
-        Rule(PathMatches("/delegate"), HTTPServerConnectionDelegateImpl())
+        Rule(PathMatches("/callable"), request_callable)
     ])
 
-    server = HTTPServer(router)
+    def request_callable(request):
+        request.write(b"HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\n\\r\\nOK")
+        request.finish()
 
-You can use nested routers as targets as well:
+3) Another `Router` instance:
 
 .. code-block:: python
 
@@ -80,9 +125,7 @@ You can use nested routers as targets as well:
         Rule(PathMatches("/router.*"), CustomRouter())
     ])
 
-    server = HTTPServer(router)
-
-And of course a nested `RuleRouter` would be a valid thing:
+Of course a nested `RuleRouter` or a `~.web.Application` is allowed:
 
 .. code-block:: python
 
@@ -94,14 +137,28 @@ And of course a nested `RuleRouter` would be a valid thing:
 
     server = HTTPServer(router)
 
-Rules are instances of `Rule` class. They contain some target (`~.web.Application` instance,
-`~.httputil.HTTPServerConnectionDelegate` implementation, a callable or a nested `Router`) and
-provide the basic routing logic defining whether this rule is a match for a particular request.
-This routing logic is implemented in `Matcher` subclasses (see `HostMatches`, `PathMatches`
-and others).
+In the example below `RuleRouter` is used to route between applications:
+
+.. code-block:: python
 
-`~URLSpec` is simply a subclass of a `Rule` with `PathMatches` matcher and is preserved for
-backwards compatibility.
+    app1 = Application([
+        (r"/app1/handler", Handler1),
+        # other handlers ...
+    ])
+
+    app2 = Application([
+        (r"/app2/handler", Handler2),
+        # other handlers ...
+    ])
+
+    router = RuleRouter([
+        Rule(PathMatches("/app1.*"), app1),
+        Rule(PathMatches("/app2.*"), app2)
+    ])
+
+    server = HTTPServer(router)
+
+For more information on application-level routing see docs for `~.web.Application`.
 """
 
 from __future__ import absolute_import, division, print_function, with_statement
@@ -187,25 +244,28 @@ class RuleRouter(Router):
     """Rule-based router implementation."""
 
     def __init__(self, rules=None):
-        """Constructs a router with an ordered list of rules::
+        """Constructs a router from an ordered list of rules::
 
             RuleRouter([
-                Rule(PathMatches("/handler"), SomeHandler),
+                Rule(PathMatches("/handler"), Target),
                 # ... more rules
             ])
 
         You can also omit explicit `Rule` constructor and use tuples of arguments::
 
             RuleRouter([
-                (PathMatches("/handler"), SomeHandler),
+                (PathMatches("/handler"), Target),
             ])
 
         `PathMatches` is a default matcher, so the example above can be simplified::
 
             RuleRouter([
-                ("/handler", SomeHandler),
+                ("/handler", Target),
             ])
 
+        In the examples above, ``Target`` can be a nested `Router` instance, an instance of
+        `~.httputil.HTTPServerConnectionDelegate` or an old-style callable, accepting a request argument.
+
         :arg rules: a list of `Rule` instances or tuples of `Rule`
             constructor arguments.
         """
@@ -323,7 +383,8 @@ class Rule(object):
             whether the rule should be considered a match for a specific
             request.
         :arg target: a Rule's target (typically a ``RequestHandler`` or
-            `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`).
+            `~.httputil.HTTPServerConnectionDelegate` subclass or even a nested `Router`,
+            depending on routing implementation).
         :arg dict target_kwargs: a dict of parameters that can be useful
             at the moment of target instantiation (for example, ``status_code``
             for a ``RequestHandler`` subclass). They end up in
@@ -502,7 +563,12 @@ class PathMatches(Matcher):
 
 
 class URLSpec(Rule):
-    """Specifies mappings between URLs and handlers."""
+    """Specifies mappings between URLs and handlers.
+
+    .. versionchanged: 4.5
+       `URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for
+       backwards compatibility.
+    """
     def __init__(self, pattern, handler, kwargs=None, name=None):
         """Parameters:
 
index 37f7cfd3a393ded719694106b96b2d9e619cd342..e97786e7388343ff00625251ac66838eb19728fc 100644 (file)
 from __future__ import absolute_import, division, print_function, with_statement
 
 from tornado.httputil import HTTPHeaders, HTTPMessageDelegate, HTTPServerConnectionDelegate, ResponseStartLine
-from tornado.routing import HostMatches, PathMatches, ReversibleRouter, Rule, RuleRouter
+from tornado.routing import HostMatches, PathMatches, ReversibleRouter, Router, Rule, RuleRouter
 from tornado.testing import AsyncHTTPTestCase
-from tornado.web import Application, RequestHandler
+from tornado.web import Application, HTTPError, RequestHandler
 from tornado.wsgi import WSGIContainer
 
 
-def get_named_handler(handler_name):
+class BasicRouter(Router):
+    def find_handler(self, request, **kwargs):
+
+        class MessageDelegate(HTTPMessageDelegate):
+            def __init__(self, connection):
+                self.connection = connection
+
+            def finish(self):
+                self.connection.write_headers(
+                    ResponseStartLine("HTTP/1.1", 200, "OK"), HTTPHeaders({"Content-Length": "2"}), b"OK"
+                )
+                self.connection.finish()
+
+        return MessageDelegate(request.connection)
+
+
+class BasicRouterTestCase(AsyncHTTPTestCase):
+    def get_app(self):
+        return BasicRouter()
+
+    def test_basic_router(self):
+        response = self.fetch("/any_request")
+        self.assertEqual(response.body, b"OK")
+
+
+resources = {}
+
+
+class GetResource(RequestHandler):
+    def get(self, path):
+        if path not in resources:
+            raise HTTPError(404)
+
+        self.finish(resources[path])
+
+
+class PostResource(RequestHandler):
+    def post(self, path):
+        resources[path] = self.request.body
+
+
+class HTTPMethodRouter(Router):
+    def __init__(self, app):
+        self.app = app
+
+    def find_handler(self, request, **kwargs):
+        handler = GetResource if request.method == "GET" else PostResource
+        return self.app.get_handler_delegate(request, handler, path_args=[request.path])
+
+
+class HTTPMethodRouterTestCase(AsyncHTTPTestCase):
+    def get_app(self):
+        return HTTPMethodRouter(Application())
+
+    def test_http_method_router(self):
+        response = self.fetch("/post_resource", method="POST", body="data")
+        self.assertEqual(response.code, 200)
+
+        response = self.fetch("/get_resource")
+        self.assertEqual(response.code, 404)
+
+        response = self.fetch("/post_resource")
+        self.assertEqual(response.code, 200)
+        self.assertEqual(response.body, b"data")
+
+
+def _get_named_handler(handler_name):
     class Handler(RequestHandler):
         def get(self, *args, **kwargs):
             if self.application.settings.get("app_name") is not None:
@@ -31,8 +97,8 @@ def get_named_handler(handler_name):
     return Handler
 
 
-FirstHandler = get_named_handler("first_handler")
-SecondHandler = get_named_handler("second_handler")
+FirstHandler = _get_named_handler("first_handler")
+SecondHandler = _get_named_handler("second_handler")
 
 
 class CustomRouter(ReversibleRouter):
index d8c28a2710f5a2c3d6da90867f67f63b9da25aad..9557a6f3fefbef6d61ef62bec89d618cbfb820ee 100644 (file)
@@ -1736,14 +1736,14 @@ def addslash(method):
 
 
 class _ApplicationRouter(ReversibleRuleRouter):
-    """Routing implementation used by `Application`.
+    """Routing implementation used internally by `Application`.
 
-    Provides a binding between `Application` and `RequestHandler` implementations.
+    Provides a binding between `Application` and `RequestHandler`.
     This implementation extends `~.routing.ReversibleRuleRouter` in a couple of ways:
         * it allows to use `RequestHandler` subclasses as `~.routing.Rule` target and
-        * it allows to use a list/tuple of rules as `~.routing.Rule` target. This list is
-        substituted with an `ApplicationRouter`, instantiated with current application and
-        the list of routes.
+        * it allows to use a list/tuple of rules as `~.routing.Rule` target.
+        ``process_rule`` implementation will substitute this list with an appropriate
+        `_ApplicationRouter` instance.
     """
 
     def __init__(self, application, rules=None):
@@ -1978,6 +1978,16 @@ class Application(ReversibleRouter):
 
     def get_handler_delegate(self, request, target_class, target_kwargs=None,
                              path_args=None, path_kwargs=None):
+        """Returns `~.httputil.HTTPMessageDelegate` that can serve a request
+        for application and `RequestHandler` subclass.
+
+        :arg httputil.HTTPServerRequest request: current HTTP request.
+        :arg RequestHandler target_class: a `RequestHandler` class.
+        :arg dict target_kwargs: keyword arguments for ``target_class`` constructor.
+        :arg list path_args: positional arguments for ``target_class`` HTTP method that
+            will be executed while handling a request (``get``, ``post`` or any other).
+        :arg dict path_kwargs: keyword arguments for ``target_class`` HTTP method.
+        """
         return _HandlerDelegate(
             self, request, target_class, target_kwargs, path_args, path_kwargs)