From 861f1bdecc64e7144bb7a7d7a9ffffc42bc17f13 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 24 Sep 2013 15:50:02 -0400 Subject: Squeeze some more performance out of the WSGI __call__ and dispatch algorithm. Change-Id: I3e25381d526866d6feae05f0dcbdca5707f86df7 --- pecan/compat/__init__.py | 2 ++ pecan/core.py | 80 +++++++++++++++++++++++++----------------------- pecan/routing.py | 42 +++++++++++++------------ 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/pecan/compat/__init__.py b/pecan/compat/__init__.py index 93883f4..c929f58 100644 --- a/pecan/compat/__init__.py +++ b/pecan/compat/__init__.py @@ -7,11 +7,13 @@ if six.PY3: from urllib.parse import quote, unquote_plus from urllib.request import urlopen, URLError from html import escape + izip = zip else: import urlparse # noqa from urllib import quote, unquote_plus # noqa from urllib2 import urlopen, URLError # noqa from cgi import escape # noqa + from itertools import izip def is_bound_method(ob): diff --git a/pecan/core.py b/pecan/core.py index 0aad76a..85ae5e6 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -13,7 +13,7 @@ import six from webob import Request, Response, exc, acceptparse -from .compat import urlparse, unquote_plus +from .compat import urlparse, unquote_plus, izip from .templating import RendererFactory from .routing import lookup_controller, NonCanonicalPath from .util import _cfg, encode_if_needed @@ -297,7 +297,6 @@ class Pecan(object): ``on_error``, and ``on_route``. :param \*args: Arguments to pass to the hooks. ''' - if hook_type in ['before', 'on_route']: hooks = state.hooks else: @@ -310,7 +309,7 @@ class Pecan(object): if hook_type == 'on_error' and isinstance(result, Response): return result - def get_args(self, req, all_params, remainder, argspec, im_self): + def get_args(self, pecan_state, all_params, remainder, argspec, im_self): ''' Determines the arguments for a controller based upon parameters passed the argument specification for the controller. @@ -329,9 +328,9 @@ class Pecan(object): args.append(im_self) # grab the routing args from nested REST controllers - if 'routing_args' in req.pecan: - remainder = req.pecan['routing_args'] + list(remainder) - del req.pecan['routing_args'] + if 'routing_args' in pecan_state: + remainder = pecan_state['routing_args'] + list(remainder) + del pecan_state['routing_args'] # handle positional arguments if valid_args and remainder: @@ -347,7 +346,7 @@ class Pecan(object): # get the default positional arguments if argspec[3]: - defaults = dict(zip(argspec[0][-len(argspec[3]):], argspec[3])) + defaults = dict(izip(argspec[0][-len(argspec[3]):], argspec[3])) else: defaults = dict() @@ -390,23 +389,24 @@ class Pecan(object): # get a sorted list of hooks, by priority (no controller hooks yet) state.hooks = self.hooks + pecan_state = req.pecan # store the routing path for the current application to allow hooks to # modify it - req.pecan['routing_path'] = req.path_info + pecan_state['routing_path'] = path = req.encget('PATH_INFO') # handle "on_route" hooks self.handle_hooks('on_route', state) # lookup the controller, respecting content-type as requested # by the file extension on the URI - path = req.pecan['routing_path'] - req.pecan['extension'] = None + pecan_state['extension'] = None + content_type = pecan_state['content_type'] # attempt to guess the content type based on the file extension if self.guess_content_type_from_ext \ - and not req.pecan['content_type'] \ - and '.' in path.split('/')[-1]: + and not content_type \ + and '.' in path: new_path, extension = splitext(path) # preface with a letter to ensure compat for 2.5 @@ -414,8 +414,8 @@ class Pecan(object): if potential_type is not None: path = new_path - req.pecan['extension'] = extension - req.pecan['content_type'] = potential_type + pecan_state['extension'] = extension + content_type = potential_type controller, remainder = self.route(req, self.root, path) cfg = _cfg(controller) @@ -436,14 +436,14 @@ class Pecan(object): # if unsure ask the controller for the default content type content_types = cfg.get('content_types', {}) - if not req.pecan['content_type']: + if not content_type: # attempt to find a best match based on accept headers (if they # exist) - accept = req.headers.get('Accept', '*/*') + accept = getattr(req.accept, 'header_value', '*/*') if accept == '*/*' or ( accept.startswith('text/html,') and list(content_types.keys()) in self.SIMPLEST_CONTENT_TYPES): - req.pecan['content_type'] = cfg.get( + content_type = cfg.get( 'content_type', 'text/html' ) @@ -460,23 +460,22 @@ class Pecan(object): logger.error( msg % ( controller.__name__, - req.pecan['content_type'], + content_type, content_types.keys() ) ) raise exc.HTTPNotAcceptable() - req.pecan['content_type'] = best_default + content_type = best_default elif cfg.get('content_type') is not None and \ - req.pecan['content_type'] not in \ - content_types: + content_type not in content_types: msg = "Controller '%s' defined does not support content_type " + \ "'%s'. Supported type(s): %s" logger.error( msg % ( controller.__name__, - req.pecan['content_type'], + content_type, content_types.keys() ) ) @@ -485,15 +484,20 @@ class Pecan(object): # get a sorted list of hooks, by priority state.hooks = self.determine_hooks(controller) + pecan_state['content_type'] = content_type + # handle "before" hooks self.handle_hooks('before', state) # fetch any parameters - params = dict(req.params) + if req.method == 'GET': + params = dict(req.GET) + else: + params = dict(req.params) # fetch the arguments for the controller args, kwargs = self.get_args( - req, + pecan_state, params, remainder, cfg['argspec'], @@ -505,27 +509,25 @@ class Pecan(object): # a controller can return the response object which means they've taken # care of filling it out - if result == response: + if result is response: return raw_namespace = result # pull the template out based upon content type and handle overrides - template = content_types.get( - req.pecan['content_type'] - ) + template = content_types.get(content_type) # check if for controller override of template - template = req.pecan.get('override_template', template) - req.pecan['content_type'] = req.pecan.get( + template = pecan_state.get('override_template', template) + content_type = pecan_state.get( 'override_content_type', - req.pecan['content_type'] + content_type ) # if there is a template, render it if template: if template == 'json': - req.pecan['content_type'] = 'application/json' + content_type = 'application/json' result = self.render(template, result) # If we are in a test request put the namespace where it can be @@ -543,8 +545,8 @@ class Pecan(object): resp.body = result # set the content type - if req.pecan['content_type']: - resp.content_type = req.pecan['content_type'] + if content_type: + resp.content_type = content_type def __call__(self, environ, start_response): ''' @@ -553,8 +555,8 @@ class Pecan(object): ''' # create the request and response object - state.request = Request(environ) - state.response = Response() + state.request = req = Request(environ) + state.response = resp = Response() state.hooks = [] state.app = self state.controller = None @@ -562,10 +564,10 @@ class Pecan(object): # handle the request try: # add context and environment to the request - state.request.context = {} - state.request.pecan = dict(content_type=None) + req.context = {} + req.pecan = dict(content_type=None) - self.handle_request(state.request, state.response) + self.handle_request(req, resp) except Exception as e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): diff --git a/pecan/routing.py b/pecan/routing.py index fc1a7d4..47eb354 100644 --- a/pecan/routing.py +++ b/pecan/routing.py @@ -8,6 +8,10 @@ from .util import iscontroller __all__ = ['lookup_controller', 'find_object'] +class PecanNotFound(Exception): + pass + + class NonCanonicalPath(Exception): ''' Exception Raised when a non-canonical path is encountered when 'walking' @@ -19,22 +23,20 @@ class NonCanonicalPath(Exception): self.remainder = remainder -def lookup_controller(obj, url_path): +def lookup_controller(obj, remainder): ''' Traverses the requested url path and returns the appropriate controller object, including default routes. Handles common errors gracefully. ''' - remainder = url_path notfound_handlers = [] - while True: try: obj, remainder = find_object(obj, remainder, notfound_handlers) handle_security(obj) return obj, remainder - except exc.HTTPNotFound: + except PecanNotFound: while notfound_handlers: name, obj, remainder = notfound_handlers.pop() if name == '_default': @@ -52,7 +54,7 @@ def lookup_controller(obj, url_path): remainder == [''] and len(obj._pecan['argspec'].args) > 1 ): - raise + raise exc.HTTPNotFound return lookup_controller(*result) else: raise exc.HTTPNotFound @@ -84,22 +86,24 @@ def find_object(obj, remainder, notfound_handlers): prev_obj = None while True: if obj is None: - raise exc.HTTPNotFound + raise PecanNotFound if iscontroller(obj): return obj, remainder # are we traversing to another controller cross_boundary(prev_obj, obj) - - if remainder and remainder[0] == '': - index = getattr(obj, 'index', None) - if iscontroller(index): - return index, remainder[1:] - elif not remainder: + try: + next_obj, rest = remainder[0], remainder[1:] + if next_obj == '': + index = getattr(obj, 'index', None) + if iscontroller(index): + return index, rest + except IndexError: # the URL has hit an index method without a trailing slash index = getattr(obj, 'index', None) if iscontroller(index): - raise NonCanonicalPath(index, remainder[1:]) + raise NonCanonicalPath(index, []) + default = getattr(obj, '_default', None) if iscontroller(default): notfound_handlers.append(('_default', default, remainder)) @@ -110,12 +114,12 @@ def find_object(obj, remainder, notfound_handlers): route = getattr(obj, '_route', None) if iscontroller(route): - next, next_remainder = route(remainder) - cross_boundary(route, next) - return next, next_remainder + next_obj, next_remainder = route(remainder) + cross_boundary(route, next_obj) + return next_obj, next_remainder if not remainder: - raise exc.HTTPNotFound - next, remainder = remainder[0], remainder[1:] + raise PecanNotFound prev_obj = obj - obj = getattr(obj, next, None) + remainder = rest + obj = getattr(obj, next_obj, None) -- cgit v1.2.1