From: Andrey Sumin Date: Thu, 15 Sep 2016 11:02:42 +0000 (+0300) Subject: [WIP] routing mechanism draft X-Git-Tag: v4.5.0~48^2~3 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=dd047596c70e449f9c3f10e05a3efe31c7f96c11;p=thirdparty%2Ftornado.git [WIP] routing mechanism draft --- diff --git a/tornado/httputil.py b/tornado/httputil.py index 21842caa2..8e9361c5c 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -352,6 +352,7 @@ class HTTPServerRequest(object): self.protocol = getattr(context, 'protocol', "http") self.host = host or self.headers.get("Host") or "127.0.0.1" + self.host_name = split_host_and_port(self.host.lower())[0] self.files = files or {} self.connection = connection self._start_time = time.time() diff --git a/tornado/routing.py b/tornado/routing.py new file mode 100644 index 000000000..ba6f9d24a --- /dev/null +++ b/tornado/routing.py @@ -0,0 +1,367 @@ +# Copyright 2015 The Tornado Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Basic implementation of rule-based routing. +""" + +from __future__ import absolute_import, division, print_function, with_statement + +import re +from functools import partial +from inspect import isclass + +from tornado import httputil +from tornado.httpserver import _CallableAdapter +from tornado.escape import url_escape, url_unescape, utf8 +from tornado.log import app_log +from tornado.util import basestring_type, import_object, re_unescape, unicode_type + +try: + import typing # noqa +except ImportError: + pass + + +class Router(httputil.HTTPServerConnectionDelegate): + """Abstract router interface. + Any `Router` instance that correctly implements `find_handler` method can be used as a ``request_callback`` + in `httpserver.HTTPServer`. + """ + def find_handler(self, request, **kwargs): + # type: (httputil.HTTPServerRequest, typing.Any)->httputil.HTTPMessageDelegate + """Must be implemented to return an appropriate instance of `httputil.HTTPMessageDelegate` + that can serve current request. + Router implementations may pass additional kwargs to extend the routing logic. + """ + raise NotImplementedError() + + def start_request(self, server_conn, request_conn): + return _RoutingDelegate(self, request_conn) + + +class ReversibleRouter(Router): + def reverse_url(self, name, *args): + raise NotImplementedError() + + +class _RoutingDelegate(httputil.HTTPMessageDelegate): + def __init__(self, router, request_conn): + self.connection = request_conn + self.delegate = None + self.router = router # type: Router + + def headers_received(self, start_line, headers): + request = httputil.HTTPServerRequest( + connection=self.connection, start_line=start_line, + headers=headers) + + self.delegate = self.router.find_handler(request) + return self.delegate.headers_received(start_line, headers) + + def data_received(self, chunk): + return self.delegate.data_received(chunk) + + def finish(self): + self.delegate.finish() + + def on_connection_close(self): + self.delegate.on_connection_close() + + +class RuleRouter(Router): + def __init__(self, rules=None): + self.rules = [] # type: typing.List[Rule] + if rules: + self.add_rules(rules) + + def add_rules(self, rules): + for rule in rules: + if isinstance(rule, (tuple, list)): + assert len(rule) in (2, 3, 4) + if isinstance(rule[0], basestring_type): + rule = Rule(PathMatches(rule[0]), *rule[1:]) + else: + rule = Rule(*rule) + + self.rules.append(self.process_rule(rule)) + + def process_rule(self, rule): + return rule + + def find_handler(self, request, **kwargs): + for rule in self.rules: + target_params = rule.matcher.match(request) + if target_params is not None: + if rule.target_kwargs: + target_params['target_kwargs'] = rule.target_kwargs + + delegate = self.get_target_delegate( + rule.target, request, **target_params) + + if delegate is not None: + return delegate + + return None + + def get_target_delegate(self, target, request, **target_params): + if isinstance(target, Router): + return target.find_handler(request, **target_params) + + elif isclass(target) and issubclass(target, httputil.HTTPMessageDelegate): + return target(request.connection) + + elif callable(target): + return _CallableAdapter( + partial(target, **target_params), request.connection + ) + + return None + + +class ReversibleRuleRouter(ReversibleRouter, RuleRouter): + def __init__(self, rules=None): + self.named_rules = {} + super(ReversibleRuleRouter, self).__init__(rules) + + def process_rule(self, rule): + rule = super(ReversibleRuleRouter, self).process_rule(rule) + + if rule.name: + if rule.name in self.named_rules: + app_log.warning( + "Multiple handlers named %s; replacing previous value", + rule.name) + self.named_rules[rule.name] = rule + + return rule + + def reverse_url(self, name, *args): + if name in self.named_rules: + return self.named_rules[name].matcher.reverse(*args) + + for rule in self.rules: + if isinstance(rule.target, ReversibleRouter): + reversed_url = rule.target.reverse_url(name, *args) + if reversed_url is not None: + return reversed_url + + return None + + +class Rule(object): + def __init__(self, matcher, target, target_kwargs=None, name=None): + if isinstance(target, str): + # import the Module and instantiate the class + # Must be a fully qualified name (module.ClassName) + target = import_object(target) + + self.matcher = matcher # type: Matcher + self.target = target + self.target_kwargs = target_kwargs if target_kwargs else {} + self.name = name + + def reverse(self, *args): + return self.matcher.reverse(*args) + + def __repr__(self): + return '%s(%r, %s, kwargs=%r, name=%r)' % \ + (self.__class__.__name__, self.matcher, + self.target, self.target_kwargs, self.name) + + +class Matcher(object): + """A complex matcher can be represented as an instance of some + `Matcher` subclass. It must implement ``__call__`` (which will be executed + with a `httpserver.HTTPRequest` argument). + """ + + def match(self, request): + """Matches an instance against the request. + + :arg tornado.httpserver.HTTPRequest request: current HTTP request + :returns a dict of parameters to be passed to the target handler + (for example, ``handler_kwargs``, ``path_args``, ``path_kwargs`` + can be passed for proper `tornado.web.RequestHandler` instantiation). + An empty dict is a valid (and common) return value to indicate a match + when the argument-passing features are not used. + ``None`` must be returned to indicate that there is no match.""" + raise NotImplementedError() + + def reverse(self, *args): + """Reconstruct URL from matcher instance""" + return None + + +class AnyMatches(Matcher): + def match(self, request): + return {} + + +class HostMatches(Matcher): + def __init__(self, host_pattern): + if isinstance(host_pattern, basestring_type): + if not host_pattern.endswith("$"): + host_pattern += "$" + self.host_pattern = re.compile(host_pattern) + else: + self.host_pattern = host_pattern + + def match(self, request): + if self.host_pattern.match(request.host_name): + return {} + + return None + + +class DefaultHostMatches(Matcher): + def __init__(self, application, host_pattern): + self.application = application + self.host_pattern = host_pattern + + def match(self, request): + # Look for default host if not behind load balancer (for debugging) + if "X-Real-Ip" not in request.headers: + if self.host_pattern.match(self.application.default_host): + return {} + return None + + +class PathMatches(Matcher): + def __init__(self, pattern): + if isinstance(pattern, basestring_type): + if not pattern.endswith('$'): + pattern += '$' + self.regex = re.compile(pattern) + else: + self.regex = pattern + + assert len(self.regex.groupindex) in (0, self.regex.groups), \ + ("groups in url regexes must either be all named or all " + "positional: %r" % self.regex.pattern) + + self._path, self._group_count = self._find_groups() + + def match(self, request): + match = self.regex.match(request.path) + if match is None: + return None + if not self.regex.groups: + return {} + + path_args, path_kwargs = [], {} + + # Pass matched groups to the handler. Since + # match.groups() includes both named and + # unnamed groups, we want to use either groups + # or groupdict but not both. + if self.regex.groupindex: + path_kwargs = dict( + (str(k), _unquote_or_none(v)) + for (k, v) in match.groupdict().items()) + else: + path_args = [_unquote_or_none(s) for s in match.groups()] + + return dict(path_args=path_args, path_kwargs=path_kwargs) + + def reverse(self, *args): + if self._path is None: + raise ValueError("Cannot reverse url regex " + self.regex.pattern) + assert len(args) == self._group_count, "required number of arguments " \ + "not found" + if not len(args): + return self._path + converted_args = [] + for a in args: + if not isinstance(a, (unicode_type, bytes)): + a = str(a) + converted_args.append(url_escape(utf8(a), plus=False)) + return self._path % tuple(converted_args) + + def _find_groups(self): + """Returns a tuple (reverse string, group count) for a url. + + For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method + would return ('/%s/%s/', 2). + """ + pattern = self.regex.pattern + if pattern.startswith('^'): + pattern = pattern[1:] + if pattern.endswith('$'): + pattern = pattern[:-1] + + if self.regex.groups != pattern.count('('): + # The pattern is too complicated for our simplistic matching, + # so we can't support reversing it. + return None, None + + pieces = [] + for fragment in pattern.split('('): + if ')' in fragment: + paren_loc = fragment.index(')') + if paren_loc >= 0: + pieces.append('%s' + fragment[paren_loc + 1:]) + else: + try: + unescaped_fragment = re_unescape(fragment) + except ValueError as exc: + # If we can't unescape part of it, we can't + # reverse this url. + return (None, None) + pieces.append(unescaped_fragment) + + return ''.join(pieces), self.regex.groups + + +class URLSpec(Rule): + """Specifies mappings between URLs and handlers.""" + def __init__(self, pattern, handler, kwargs=None, name=None): + """Parameters: + + * ``pattern``: Regular expression to be matched. Any capturing + groups in the regex will be passed in to the handler's + get/post/etc methods as arguments (by keyword if named, by + position if unnamed. Named and unnamed capturing groups may + may not be mixed in the same rule). + + * ``handler``: `RequestHandler` subclass to be invoked. + + * ``kwargs`` (optional): A dictionary of additional arguments + to be passed to the handler's constructor. + + * ``name`` (optional): A name for this handler. Used by + `Application.reverse_url`. + + """ + super(URLSpec, self).__init__(PathMatches(pattern), handler, kwargs, name) + + self.regex = self.matcher.regex + self.handler_class = self.target + self.kwargs = kwargs + + def __repr__(self): + return '%s(%r, %s, kwargs=%r, name=%r)' % \ + (self.__class__.__name__, self.regex.pattern, + self.handler_class, self.kwargs, self.name) + + +def _unquote_or_none(s): + """None-safe wrapper around url_unescape to handle unmatched optional + groups correctly. + + Note that args are passed as bytes so the handler can decide what + encoding to use. + """ + if s is None: + return s + return url_unescape(s, encoding=None, plus=False) diff --git a/tornado/test/routing_test.py b/tornado/test/routing_test.py new file mode 100644 index 000000000..1d1a858b0 --- /dev/null +++ b/tornado/test/routing_test.py @@ -0,0 +1,153 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from __future__ import absolute_import, division, print_function, with_statement + +from tornado.httputil import HTTPHeaders, HTTPMessageDelegate, ResponseStartLine +from tornado.routing import HostMatches, PathMatches, ReversibleRouter, Rule, RuleRouter +from tornado.testing import AsyncHTTPTestCase +from tornado.web import Application, RequestHandler +from tornado.wsgi import WSGIContainer + + +def get_named_handler(handler_name): + class Handler(RequestHandler): + def get(self, *args, **kwargs): + if self.application.settings.get("app_name") is not None: + self.write(self.application.settings["app_name"] + ": ") + + self.finish(handler_name + ": " + self.reverse_url(handler_name)) + + return Handler + + +FirstHandler = get_named_handler("first_handler") +SecondHandler = get_named_handler("second_handler") + + +class CustomRouter(ReversibleRouter): + def __init__(self): + super(CustomRouter, self).__init__() + self.routes = {} + + def add_routes(self, routes): + self.routes.update(routes) + + def find_handler(self, request, **kwargs): + if request.path in self.routes: + app, handler = self.routes[request.path] + return app.get_handler_delegate(request, handler) + + def reverse_url(self, name, *args): + handler_path = '/' + name + return handler_path if handler_path in self.routes else None + + +class CustomRouterTestCase(AsyncHTTPTestCase): + def get_app(self): + class CustomApplication(Application): + def reverse_url(self, name, *args): + return router.reverse_url(name, *args) + + router = CustomRouter() + app1 = CustomApplication(app_name="app1") + app2 = CustomApplication(app_name="app2") + + router.add_routes({ + "/first_handler": (app1, FirstHandler), + "/second_handler": (app2, SecondHandler), + "/first_handler_second_app": (app2, FirstHandler), + }) + + return router + + def test_custom_router(self): + response = self.fetch("/first_handler") + self.assertEqual(response.body, b"app1: first_handler: /first_handler") + response = self.fetch("/second_handler") + self.assertEqual(response.body, b"app2: second_handler: /second_handler") + response = self.fetch("/first_handler_second_app") + self.assertEqual(response.body, b"app2: first_handler: /first_handler") + + +class MessageDelegate(HTTPMessageDelegate): + def __init__(self, connection): + self.connection = connection + + def finish(self): + response_body = b"OK" + self.connection.write_headers( + ResponseStartLine("HTTP/1.1", 200, "OK"), + HTTPHeaders({"Content-Length": str(len(response_body))})) + self.connection.write(response_body) + self.connection.finish() + + +class RuleRouterTest(AsyncHTTPTestCase): + def get_app(self): + app = Application() + + def request_callable(request): + request.write(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK") + request.finish() + + app.add_handlers(".*", [ + (HostMatches("www.example.com"), [ + (PathMatches("/first_handler"), "tornado.test.routing_test.SecondHandler", {}, "second_handler") + ]), + Rule(PathMatches("/first_handler"), FirstHandler, name="first_handler"), + Rule(PathMatches("/request_callable"), request_callable), + ("/message_delegate", MessageDelegate) + ]) + + return app + + def test_rule_based_router(self): + response = self.fetch("/first_handler") + self.assertEqual(response.body, b"first_handler: /first_handler") + response = self.fetch("/first_handler", headers={'Host': 'www.example.com'}) + self.assertEqual(response.body, b"second_handler: /first_handler") + + response = self.fetch("/message_delegate") + self.assertEqual(response.body, b"OK") + + response = self.fetch("/request_callable") + self.assertEqual(response.body, b"OK") + + response = self.fetch("/404") + self.assertEqual(response.code, 404) + + +class WSGIContainerTestCase(AsyncHTTPTestCase): + def get_app(self): + wsgi_app = WSGIContainer(self.wsgi_app) + + class Handler(RequestHandler): + def get(self, *args, **kwargs): + self.finish(self.reverse_url("tornado")) + + return RuleRouter([ + (PathMatches("/tornado.*"), Application([(r"/tornado/test", Handler, {}, "tornado")])), + (PathMatches("/wsgi"), wsgi_app), + ]) + + def wsgi_app(self, environ, start_response): + start_response("200 OK", []) + return [b"WSGI"] + + def test_wsgi_container(self): + response = self.fetch("/tornado/test") + self.assertEqual(response.body, b"/tornado/test") + + response = self.fetch("/wsgi") + self.assertEqual(response.body, b"WSGI") diff --git a/tornado/test/runtests.py b/tornado/test/runtests.py index f4dd46de3..5dc4abe6b 100644 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -43,6 +43,7 @@ TEST_MODULES = [ 'tornado.test.options_test', 'tornado.test.process_test', 'tornado.test.queues_test', + 'tornado.test.routing_test', 'tornado.test.simple_httpclient_test', 'tornado.test.stack_context_test', 'tornado.test.tcpclient_test', diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index b09bb9abf..ff722c653 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -1348,6 +1348,8 @@ class HostMatchingTest(WebTestCase): [("/bar", HostMatchingTest.Handler, {"reply": "[1]"})]) self.app.add_handlers("www.example.com", [("/baz", HostMatchingTest.Handler, {"reply": "[2]"})]) + self.app.add_handlers("www.e.*e.com", + [("/baz", HostMatchingTest.Handler, {"reply": "[3]"})]) response = self.fetch("/foo") self.assertEqual(response.body, b"wildcard") @@ -1362,6 +1364,8 @@ class HostMatchingTest(WebTestCase): self.assertEqual(response.body, b"[1]") response = self.fetch("/baz", headers={'Host': 'www.example.com'}) self.assertEqual(response.body, b"[2]") + response = self.fetch("/baz", headers={'Host': 'www.exe.com'}) + self.assertEqual(response.body, b"[3]") @wsgi_safe @@ -1387,8 +1391,8 @@ class DefaultHostMatchingTest(WebTestCase): response = self.fetch("/baz") self.assertEqual(response.code, 404) - response = self.fetch("/foo", follow_redirects=False, headers={"X-Real-Ip": "127.0.0.1"}) - self.assertEqual(response.code, 301) + response = self.fetch("/foo", headers={"X-Real-Ip": "127.0.0.1"}) + self.assertEqual(response.code, 404) self.app.default_host = "www.test.com" diff --git a/tornado/web.py b/tornado/web.py index f4c50e3c9..5d675ff84 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -77,6 +77,7 @@ import time import tornado import traceback import types +from inspect import isclass from io import BytesIO from tornado.concurrent import Future @@ -89,9 +90,13 @@ from tornado.log import access_log, app_log, gen_log from tornado import stack_context from tornado import template from tornado.escape import utf8, _unicode -from tornado.util import (import_object, ObjectDict, raise_exc_info, - unicode_type, _websocket_mask, re_unescape, PY3) -from tornado.httputil import split_host_and_port +from tornado.routing import (AnyMatches, DefaultHostMatches, HostMatches, + ReversibleRouter, Rule, ReversibleRuleRouter, + URLSpec) +from tornado.util import (ObjectDict, raise_exc_info, + unicode_type, _websocket_mask, PY3) + +url = URLSpec if PY3: import http.cookies as Cookie @@ -1727,7 +1732,28 @@ def addslash(method): return wrapper -class Application(httputil.HTTPServerConnectionDelegate): +class ApplicationRouter(ReversibleRuleRouter): + def __init__(self, application, rules=None): + assert isinstance(application, Application) + self.application = application + super(ApplicationRouter, self).__init__(rules) + + def process_rule(self, rule): + rule = super(ApplicationRouter, self).process_rule(rule) + + if isinstance(rule.target, (list, tuple)): + rule.target = ApplicationRouter(self.application, rule.target) + + return rule + + def get_target_delegate(self, target, request, **target_params): + if isclass(target) and issubclass(target, RequestHandler): + return self.application.get_handler_delegate(request, target, **target_params) + + return super(ApplicationRouter, self).get_target_delegate(target, request, **target_params) + + +class Application(ReversibleRouter): """A collection of request handlers that make up a web application. Instances of this class are callable and can be passed directly to @@ -1781,7 +1807,7 @@ class Application(httputil.HTTPServerConnectionDelegate): ``static_handler_class`` setting. """ - def __init__(self, handlers=None, default_host="", transforms=None, + def __init__(self, handlers=None, default_host=None, transforms=None, **settings): if transforms is None: self.transforms = [] @@ -1789,8 +1815,6 @@ class Application(httputil.HTTPServerConnectionDelegate): self.transforms.append(GZipContentEncoding) else: self.transforms = transforms - self.handlers = [] - self.named_handlers = {} self.default_host = default_host self.settings = settings self.ui_modules = {'linkify': _linkify, @@ -1813,8 +1837,6 @@ class Application(httputil.HTTPServerConnectionDelegate): r"/(favicon\.ico)", r"/(robots\.txt)"]: handlers.insert(0, (pattern, static_handler_class, static_handler_args)) - if handlers: - self.add_handlers(".*$", handlers) if self.settings.get('debug'): self.settings.setdefault('autoreload', True) @@ -1822,6 +1844,13 @@ class Application(httputil.HTTPServerConnectionDelegate): self.settings.setdefault('static_hash_cache', False) self.settings.setdefault('serve_traceback', True) + self.wildcard_router = ApplicationRouter(self, handlers) + + self.default_router = ApplicationRouter(self) + self.default_router.add_rules([ + Rule(AnyMatches(), self.wildcard_router) + ]) + # Automatically reload modified modules if self.settings.get('autoreload'): from tornado import autoreload @@ -1859,47 +1888,20 @@ class Application(httputil.HTTPServerConnectionDelegate): Host patterns are processed sequentially in the order they were added. All matching patterns will be considered. """ - if not host_pattern.endswith("$"): - host_pattern += "$" - handlers = [] - # The handlers with the wildcard host_pattern are a special - # case - they're added in the constructor but should have lower - # precedence than the more-precise handlers added later. - # If a wildcard handler group exists, it should always be last - # in the list, so insert new groups just before it. - if self.handlers and self.handlers[-1][0].pattern == '.*$': - self.handlers.insert(-1, (re.compile(host_pattern), handlers)) - else: - self.handlers.append((re.compile(host_pattern), handlers)) - - for spec in host_handlers: - if isinstance(spec, (tuple, list)): - assert len(spec) in (2, 3, 4) - spec = URLSpec(*spec) - handlers.append(spec) - if spec.name: - if spec.name in self.named_handlers: - app_log.warning( - "Multiple handlers named %s; replacing previous value", - spec.name) - self.named_handlers[spec.name] = spec + host_matcher = HostMatches(host_pattern) + rule = Rule(host_matcher, ApplicationRouter(self, host_handlers)) + + self.default_router.rules.insert(-1, rule) + + if self.default_host is not None: + self.wildcard_router.add_rules([( + DefaultHostMatches(self, host_matcher.host_pattern), + host_handlers + )]) def add_transform(self, transform_class): self.transforms.append(transform_class) - def _get_host_handlers(self, request): - host = split_host_and_port(request.host.lower())[0] - matches = [] - for pattern, handlers in self.handlers: - if pattern.match(host): - matches.extend(handlers) - # Look for default host if not behind load balancer (for debugging) - if not matches and "X-Real-Ip" not in request.headers: - for pattern, handlers in self.handlers: - if pattern.match(self.default_host): - matches.extend(handlers) - return matches or None - def _load_ui_methods(self, methods): if isinstance(methods, types.ModuleType): self._load_ui_methods(dict((n, getattr(methods, n)) @@ -1929,16 +1931,30 @@ class Application(httputil.HTTPServerConnectionDelegate): except TypeError: pass - def start_request(self, server_conn, request_conn): - # Modern HTTPServer interface - return _RequestDispatcher(self, request_conn) - def __call__(self, request): # Legacy HTTPServer interface - dispatcher = _RequestDispatcher(self, None) - dispatcher.set_request(request) + dispatcher = self.find_handler(request) return dispatcher.execute() + def find_handler(self, request): + route = self.default_router.find_handler(request) + if route is not None: + return route + + if self.settings.get('default_handler_class'): + return self.get_handler_delegate( + request, + self.settings['default_handler_class'], + self.settings.get('default_handler_args', {})) + + return self.get_handler_delegate( + request, ErrorHandler, {'status_code': 404}) + + def get_handler_delegate(self, request, target_class, target_kwargs=None, + path_args=None, path_kwargs=None): + return _HandlerDelegate( + self, request, target_class, target_kwargs, path_args, path_kwargs) + def reverse_url(self, name, *args): """Returns a URL path for handler named ``name`` @@ -1948,8 +1964,10 @@ class Application(httputil.HTTPServerConnectionDelegate): They will be converted to strings if necessary, encoded as utf8, and url-escaped. """ - if name in self.named_handlers: - return self.named_handlers[name].reverse(*args) + reversed_url = self.default_router.reverse_url(name, *args) + if reversed_url is not None: + return reversed_url + raise KeyError("%s not found in named urls" % name) def log_request(self, handler): @@ -1974,67 +1992,24 @@ class Application(httputil.HTTPServerConnectionDelegate): handler._request_summary(), request_time) -class _RequestDispatcher(httputil.HTTPMessageDelegate): - def __init__(self, application, connection): +class _HandlerDelegate(httputil.HTTPMessageDelegate): + def __init__(self, application, request, handler_class, handler_kwargs, + path_args, path_kwargs): self.application = application - self.connection = connection - self.request = None + self.connection = request.connection + self.request = request + self.handler_class = handler_class + self.handler_kwargs = handler_kwargs or {} + self.path_args = path_args or [] + self.path_kwargs = path_kwargs or {} self.chunks = [] - self.handler_class = None - self.handler_kwargs = None - self.path_args = [] - self.path_kwargs = {} + self.stream_request_body = _has_stream_request_body(self.handler_class) def headers_received(self, start_line, headers): - self.set_request(httputil.HTTPServerRequest( - connection=self.connection, start_line=start_line, - headers=headers)) if self.stream_request_body: self.request.body = Future() return self.execute() - def set_request(self, request): - self.request = request - self._find_handler() - self.stream_request_body = _has_stream_request_body(self.handler_class) - - def _find_handler(self): - # Identify the handler to use as soon as we have the request. - # Save url path arguments for later. - app = self.application - handlers = app._get_host_handlers(self.request) - if not handlers: - self.handler_class = RedirectHandler - self.handler_kwargs = dict(url="%s://%s/" - % (self.request.protocol, - app.default_host)) - return - for spec in handlers: - match = spec.regex.match(self.request.path) - if match: - self.handler_class = spec.handler_class - self.handler_kwargs = spec.kwargs - if spec.regex.groups: - # Pass matched groups to the handler. Since - # match.groups() includes both named and - # unnamed groups, we want to use either groups - # or groupdict but not both. - if spec.regex.groupindex: - self.path_kwargs = dict( - (str(k), _unquote_or_none(v)) - for (k, v) in match.groupdict().items()) - else: - self.path_args = [_unquote_or_none(s) - for s in match.groups()] - return - if app.settings.get('default_handler_class'): - self.handler_class = app.settings['default_handler_class'] - self.handler_kwargs = app.settings.get( - 'default_handler_args', {}) - else: - self.handler_class = ErrorHandler - self.handler_kwargs = dict(status_code=404) - def data_received(self, data): if self.stream_request_body: return self.handler.data_received(data) @@ -3009,99 +2984,6 @@ class _UIModuleNamespace(object): raise AttributeError(str(e)) -class URLSpec(object): - """Specifies mappings between URLs and handlers.""" - def __init__(self, pattern, handler, kwargs=None, name=None): - """Parameters: - - * ``pattern``: Regular expression to be matched. Any capturing - groups in the regex will be passed in to the handler's - get/post/etc methods as arguments (by keyword if named, by - position if unnamed. Named and unnamed capturing groups may - may not be mixed in the same rule). - - * ``handler``: `RequestHandler` subclass to be invoked. - - * ``kwargs`` (optional): A dictionary of additional arguments - to be passed to the handler's constructor. - - * ``name`` (optional): A name for this handler. Used by - `Application.reverse_url`. - - """ - if not pattern.endswith('$'): - pattern += '$' - self.regex = re.compile(pattern) - assert len(self.regex.groupindex) in (0, self.regex.groups), \ - ("groups in url regexes must either be all named or all " - "positional: %r" % self.regex.pattern) - - if isinstance(handler, str): - # import the Module and instantiate the class - # Must be a fully qualified name (module.ClassName) - handler = import_object(handler) - - self.handler_class = handler - self.kwargs = kwargs or {} - self.name = name - self._path, self._group_count = self._find_groups() - - def __repr__(self): - return '%s(%r, %s, kwargs=%r, name=%r)' % \ - (self.__class__.__name__, self.regex.pattern, - self.handler_class, self.kwargs, self.name) - - def _find_groups(self): - """Returns a tuple (reverse string, group count) for a url. - - For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method - would return ('/%s/%s/', 2). - """ - pattern = self.regex.pattern - if pattern.startswith('^'): - pattern = pattern[1:] - if pattern.endswith('$'): - pattern = pattern[:-1] - - if self.regex.groups != pattern.count('('): - # The pattern is too complicated for our simplistic matching, - # so we can't support reversing it. - return (None, None) - - pieces = [] - for fragment in pattern.split('('): - if ')' in fragment: - paren_loc = fragment.index(')') - if paren_loc >= 0: - pieces.append('%s' + fragment[paren_loc + 1:]) - else: - try: - unescaped_fragment = re_unescape(fragment) - except ValueError as exc: - # If we can't unescape part of it, we can't - # reverse this url. - return (None, None) - pieces.append(unescaped_fragment) - - return (''.join(pieces), self.regex.groups) - - def reverse(self, *args): - if self._path is None: - raise ValueError("Cannot reverse url regex " + self.regex.pattern) - assert len(args) == self._group_count, "required number of arguments "\ - "not found" - if not len(args): - return self._path - converted_args = [] - for a in args: - if not isinstance(a, (unicode_type, bytes)): - a = str(a) - converted_args.append(escape.url_escape(utf8(a), plus=False)) - return self._path % tuple(converted_args) - -url = URLSpec - - if hasattr(hmac, 'compare_digest'): # python 3.3 _time_independent_equals = hmac.compare_digest else: @@ -3322,15 +3204,3 @@ def _create_signature_v2(secret, s): hash = hmac.new(utf8(secret), digestmod=hashlib.sha256) hash.update(utf8(s)) return utf8(hash.hexdigest()) - - -def _unquote_or_none(s): - """None-safe wrapper around url_unescape to handle unmatched optional - groups correctly. - - Note that args are passed as bytes so the handler can decide what - encoding to use. - """ - if s is None: - return s - return escape.url_unescape(s, encoding=None, plus=False)