Squeeze some more performance out of the WSGI __call__ and dispatch algorithm.
Change-Id: I3e25381d526866d6feae05f0dcbdca5707f86df7
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user