diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2013-04-25 10:23:23 -0400 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2013-04-25 10:23:23 -0400 |
commit | 2ab1c2796afb47f634ffb46efd0bcfefbca82978 (patch) | |
tree | 0c929d6f0b2941336f185e667535a4a1efc3a0b0 | |
parent | 20d38c7bfc10043befa2a6a985d87d5147edfd8d (diff) | |
parent | d8a7d5d75328b82268e9175588f0737447bac835 (diff) | |
download | pecan-2ab1c2796afb47f634ffb46efd0bcfefbca82978.tar.gz |
Merge branch 'next' of github.com:dreamhost/pecan into next
Conflicts:
pecan/core.py
-rw-r--r-- | pecan/core.py | 127 | ||||
-rw-r--r-- | pecan/templating.py | 2 | ||||
-rw-r--r-- | pecan/tests/test_hooks.py | 28 | ||||
-rw-r--r-- | pecan/util.py | 11 |
4 files changed, 88 insertions, 80 deletions
diff --git a/pecan/core.py b/pecan/core.py index e1662ac..46b160a 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -9,6 +9,7 @@ from mimetypes import guess_type, add_type from urlparse import urlsplit, urlunsplit from os.path import splitext import logging +import operator from webob import Request, Response, exc, acceptparse @@ -187,6 +188,11 @@ class Pecan(object): the content type to return. ''' + SIMPLEST_CONTENT_TYPES = ( + ['text/html'], + ['text/plain'] + ) + def __init__(self, root, default_renderer='mako', template_path='templates', hooks=[], custom_renderers={}, extra_template_vars={}, force_canonical=True, @@ -198,7 +204,11 @@ class Pecan(object): self.root = root self.renderers = RendererFactory(custom_renderers, extra_template_vars) self.default_renderer = default_renderer - self.hooks = hooks + # pre-sort these so we don't have to do it per-request + self.hooks = list(sorted( + hooks, + key=operator.attrgetter('priority') + )) self.template_path = template_path self.force_canonical = force_canonical self.guess_content_type_from_ext = guess_content_type_from_ext @@ -226,7 +236,7 @@ class Pecan(object): raise ImportError('No item named %s' % item) - def route(self, node, path): + def route(self, req, node, path): ''' Looks up a controller from a node based upon the specified path. @@ -241,14 +251,14 @@ class Pecan(object): except NonCanonicalPath, e: if self.force_canonical and \ not _cfg(e.controller).get('accept_noncanonical', False): - if request.method == 'POST': + if req.method == 'POST': raise RuntimeError( "You have POSTed to a URL '%s' which " "requires a slash. Most browsers will not maintain " "POST data when redirected. Please update your code " "to POST to '%s/' or set force_canonical to False" % - (request.pecan['routing_path'], - request.pecan['routing_path']) + (req.pecan['routing_path'], + req.pecan['routing_path']) ) redirect(code=302, add_slash=True) return e.controller, e.remainder @@ -264,12 +274,14 @@ class Pecan(object): controller_hooks = [] if controller: controller_hooks = _cfg(controller).get('hooks', []) - return list( - sorted( - chain(controller_hooks, self.hooks), - lambda x, y: cmp(x.priority, y.priority) - ) - ) + if controller_hooks: + return list( + sorted( + chain(controller_hooks, self.hooks), + key=operator.attrgetter('priority') + ) + ) + return self.hooks def handle_hooks(self, hook_type, *args): ''' @@ -288,7 +300,7 @@ class Pecan(object): for hook in hooks: getattr(hook, hook_type)(*args) - def get_args(self, all_params, remainder, argspec, im_self): + def get_args(self, req, all_params, remainder, argspec, im_self): ''' Determines the arguments for a controller based upon parameters passed the argument specification for the controller. @@ -306,9 +318,9 @@ class Pecan(object): args.append(im_self) # grab the routing args from nested REST controllers - if 'routing_args' in request.pecan: - remainder = request.pecan['routing_args'] + list(remainder) - del request.pecan['routing_args'] + if 'routing_args' in req.pecan: + remainder = req.pecan['routing_args'] + list(remainder) + del req.pecan['routing_args'] # handle positional arguments if valid_args and remainder: @@ -360,29 +372,29 @@ class Pecan(object): template = template.split(':')[1] return renderer.render(template, namespace) - def handle_request(self): + def handle_request(self, req, resp): ''' The main request handler for Pecan applications. ''' # get a sorted list of hooks, by priority (no controller hooks yet) - state.hooks = self.determine_hooks() + state.hooks = self.hooks # store the routing path for the current application to allow hooks to # modify it - request.pecan['routing_path'] = request.path_info + req.pecan['routing_path'] = req.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 = request.pecan['routing_path'] - request.pecan['extension'] = None + path = req.pecan['routing_path'] + req.pecan['extension'] = None # attempt to guess the content type based on the file extension if self.guess_content_type_from_ext \ - and not request.pecan['content_type'] \ + and not req.pecan['content_type'] \ and '.' in path.split('/')[-1]: new_path, extension = splitext(path) @@ -391,10 +403,10 @@ class Pecan(object): if potential_type is not None: path = new_path - request.pecan['extension'] = extension - request.pecan['content_type'] = potential_type + req.pecan['extension'] = extension + req.pecan['content_type'] = potential_type - controller, remainder = self.route(self.root, path) + controller, remainder = self.route(req, self.root, path) cfg = _cfg(controller) if cfg.get('generic_handler'): @@ -405,21 +417,30 @@ class Pecan(object): if cfg.get('generic'): im_self = controller.im_self handlers = cfg['generic_handlers'] - controller = handlers.get(request.method, handlers['DEFAULT']) + controller = handlers.get(req.method, handlers['DEFAULT']) cfg = _cfg(controller) # add the controller to the state so that hooks can use it state.controller = controller # if unsure ask the controller for the default content type - if not request.pecan['content_type']: + content_types = cfg.get('content_types', {}) + if not req.pecan['content_type']: # attempt to find a best match based on accept headers (if they # exist) - if 'Accept' in request.headers: + accept = req.headers.get('Accept', '*/*') + 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', + 'text/html' + ) + else: best_default = acceptparse.MIMEAccept( - request.headers['Accept'] + accept ).best_match( - cfg.get('content_types', {}).keys() + content_types.keys() ) if best_default is None: @@ -428,29 +449,24 @@ class Pecan(object): logger.error( msg % ( controller.__name__, - request.pecan['content_type'], - cfg.get('content_types', {}).keys() + req.pecan['content_type'], + content_types.keys() ) ) raise exc.HTTPNotAcceptable() - request.pecan['content_type'] = best_default - else: - request.pecan['content_type'] = cfg.get( - 'content_type', - 'text/html' - ) + req.pecan['content_type'] = best_default elif cfg.get('content_type') is not None and \ - request.pecan['content_type'] not in \ - cfg.get('content_types', {}): + req.pecan['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__, - request.pecan['content_type'], - cfg.get('content_types', {}).keys() + req.pecan['content_type'], + content_types.keys() ) ) raise exc.HTTPNotFound @@ -462,10 +478,11 @@ class Pecan(object): self.handle_hooks('before', state) # fetch any parameters - params = dict(request.params) + params = dict(req.params) # fetch the arguments for the controller args, kwargs = self.get_args( + req, params, remainder, cfg['argspec'], @@ -483,40 +500,40 @@ class Pecan(object): raw_namespace = result # pull the template out based upon content type and handle overrides - template = cfg.get('content_types', {}).get( - request.pecan['content_type'] + template = content_types.get( + req.pecan['content_type'] ) # check if for controller override of template - template = request.pecan.get('override_template', template) - request.pecan['content_type'] = request.pecan.get( + template = req.pecan.get('override_template', template) + req.pecan['content_type'] = req.pecan.get( 'override_content_type', - request.pecan['content_type'] + req.pecan['content_type'] ) # if there is a template, render it if template: if template == 'json': - request.pecan['content_type'] = 'application/json' + req.pecan['content_type'] = 'application/json' result = self.render(template, result) # If we are in a test request put the namespace where it can be # accessed directly - if request.environ.get('paste.testing'): - testing_variables = request.environ['paste.testing_variables'] + if req.environ.get('paste.testing'): + testing_variables = req.environ['paste.testing_variables'] testing_variables['namespace'] = raw_namespace testing_variables['template_name'] = template testing_variables['controller_output'] = result # set the body content if isinstance(result, unicode): - response.unicode_body = result + resp.unicode_body = result else: - response.body = result + resp.body = result # set the content type - if request.pecan['content_type']: - response.content_type = request.pecan['content_type'] + if req.pecan['content_type']: + resp.content_type = req.pecan['content_type'] def __call__(self, environ, start_response): ''' @@ -537,7 +554,7 @@ class Pecan(object): state.request.context = {} state.request.pecan = dict(content_type=None) - self.handle_request() + self.handle_request(state.request, state.response) except Exception, e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): diff --git a/pecan/templating.py b/pecan/templating.py index d131909..774cce0 100644 --- a/pecan/templating.py +++ b/pecan/templating.py @@ -1,4 +1,5 @@ import cgi +from jsonify import encode _builtin_renderers = {} error_formatters = [] @@ -19,7 +20,6 @@ class JsonRenderer(object): ''' Implements ``JSON`` rendering. ''' - from jsonify import encode return encode(namespace) # TODO: add error formatter for json (pass it through json lint?) diff --git a/pecan/tests/test_hooks.py b/pecan/tests/test_hooks.py index 8f6c55a..8e103b2 100644 --- a/pecan/tests/test_hooks.py +++ b/pecan/tests/test_hooks.py @@ -144,8 +144,10 @@ class TestHooks(PecanTestCase): return 'Hello, World!' class SimpleHook(PecanHook): - def __init__(self, id): + def __init__(self, id, priority=None): self.id = str(id) + if priority: + self.priority = priority def on_route(self, state): run_hook.append('on_route' + self.id) @@ -160,7 +162,7 @@ class TestHooks(PecanTestCase): run_hook.append('error' + self.id) papp = make_app(RootController(), hooks=[ - SimpleHook(1), SimpleHook(2), SimpleHook(3) + SimpleHook(1, 3), SimpleHook(2, 2), SimpleHook(3, 1) ]) app = TestApp(papp) response = app.get('/') @@ -168,28 +170,6 @@ class TestHooks(PecanTestCase): assert response.body == 'Hello, World!' assert len(run_hook) == 10 - assert run_hook[0] == 'on_route1' - assert run_hook[1] == 'on_route2' - assert run_hook[2] == 'on_route3' - assert run_hook[3] == 'before1' - assert run_hook[4] == 'before2' - assert run_hook[5] == 'before3' - assert run_hook[6] == 'inside' - assert run_hook[7] == 'after3' - assert run_hook[8] == 'after2' - assert run_hook[9] == 'after1' - - run_hook = [] - - state.app.hooks[0].priority = 3 - state.app.hooks[1].priority = 2 - state.app.hooks[2].priority = 1 - - response = app.get('/') - assert response.status_int == 200 - assert response.body == 'Hello, World!' - - assert len(run_hook) == 10 assert run_hook[0] == 'on_route3' assert run_hook[1] == 'on_route2' assert run_hook[2] == 'on_route1' diff --git a/pecan/util.py b/pecan/util.py index aa2e683..cf62f97 100644 --- a/pecan/util.py +++ b/pecan/util.py @@ -1,10 +1,21 @@ import sys +def memodict(f): + """ Memoization decorator for a function taking a single argument """ + class memodict(dict): + def __missing__(self, key): + ret = self[key] = f(key) + return ret + return memodict().__getitem__ + + +@memodict def iscontroller(obj): return getattr(obj, 'exposed', False) +@memodict def _cfg(f): if not hasattr(f, '_pecan'): f._pecan = {} |