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()
--- /dev/null
+# 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)
--- /dev/null
+# 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")
'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',
[("/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")
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
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"
import tornado
import traceback
import types
+from inspect import isclass
from io import BytesIO
from tornado.concurrent import Future
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
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
``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 = []
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,
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)
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
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))
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``
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):
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)
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:
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)