diff --git a/pecan/core.py b/pecan/core.py index 143b091..9988cce 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -28,6 +28,7 @@ def proxy(key): return getattr(obj, attr) def __setattr__(self, attr, value): obj = getattr(state, key) + return setattr(obj, attr, value) return ObjectProxy() @@ -40,7 +41,6 @@ response = proxy('response') def override_template(template): request.override_template = template - def abort(status_code=None, detail='', headers=None, comment=None): raise exc.status_map[status_code](detail=detail, headers=headers, comment=comment) @@ -83,7 +83,8 @@ class Pecan(MonitorableProcess): return { 'html' : 'text/html', 'xhtml' : 'text/html', - 'json' : 'application/json' + 'json' : 'application/json', + 'txt' : 'text/plain' }.get(format, 'text/html') def route(self, node, path): @@ -154,19 +155,20 @@ class Pecan(MonitorableProcess): # get a sorted list of hooks, by priority (no controller hooks yet) state.hooks = self.determine_hooks() - + state.content_type = None + # 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 = state.request.path - content_type = None - if '.' in path.split('/')[-1]: + + if state.content_type is None and '.' in path.split('/')[-1]: path, format = path.split('.') - content_type = self.get_content_type(format) + state.content_type = self.get_content_type(format) controller, remainder = self.route(self.root, path) - + if controller.pecan.get('generic_handler'): raise exc.HTTPNotFound @@ -180,10 +182,9 @@ class Pecan(MonitorableProcess): # add the controller to the state so that hooks can use it state.controller = controller - # determine content type - if content_type is None: - content_type = controller.pecan.get('content_type', 'text/html') - + # if unsure ask the controller for the default content type + if state.content_type is None: + state.content_type = controller.pecan.get('content_type', 'text/html') # get a sorted list of hooks, by priority state.hooks = self.determine_hooks(controller) @@ -216,17 +217,24 @@ class Pecan(MonitorableProcess): # get the result from the controller result = controller(*positional_params, **params) + + # a controller can return the response object which means they've taken + # care of filling it out + if result == response: + return + raw_namespace = result # pull the template out based upon content type and handle overrides - template = controller.pecan.get('content_types', {}).get(content_type) + template = controller.pecan.get('content_types', {}).get(state.content_type) template = getattr(request, 'override_template', template) - + # if there is a template, render it if template: renderer = self.renderers.get(self.default_renderer, self.template_path) if template == 'json': renderer = self.renderers.get('json', self.template_path) + state.content_type = self.get_content_type('json') else: result['error_for'] = error_for @@ -234,7 +242,6 @@ class Pecan(MonitorableProcess): renderer = self.renderers.get(template.split(':')[0], self.template_path) template = template.split(':')[1] result = renderer.render(template, result) - content_type = renderer.content_type # If we are in a test request put the namespace where it can be # accessed directly @@ -251,8 +258,8 @@ class Pecan(MonitorableProcess): state.response.body = result # set the content type - if content_type: - state.response.content_type = content_type + if state.content_type: + state.response.content_type = state.content_type def __call__(self, environ, start_response): # create the request and response object @@ -287,8 +294,9 @@ class Pecan(MonitorableProcess): return state.response(environ, start_response) finally: # clean up state + del state.content_type + del state.hooks del state.request del state.response - del state.hooks if hasattr(state, 'controller'): del state.controller diff --git a/pecan/templating.py b/pecan/templating.py index cf6ab7d..d784620 100644 --- a/pecan/templating.py +++ b/pecan/templating.py @@ -7,8 +7,6 @@ _builtin_renderers = {} # class JsonRenderer(object): - content_type = 'application/json' - def __init__(self, path, extra_vars): pass @@ -27,8 +25,6 @@ try: from genshi.template import TemplateLoader class GenshiRenderer(object): - content_type = 'text/html' - def __init__(self, path, extra_vars): self.loader = TemplateLoader([path], auto_reload=True) self.extra_vars = extra_vars @@ -51,8 +47,6 @@ try: from mako.lookup import TemplateLookup class MakoRenderer(object): - content_type = 'text/html' - def __init__(self, path, extra_vars): self.loader = TemplateLookup(directories=[path]) self.extra_vars = extra_vars @@ -74,8 +68,6 @@ try: from kajiki.loader import FileLoader class KajikiRenderer(object): - content_type = 'text/html' - def __init__(self, path, extra_vars): self.loader = FileLoader(path, reload=True) self.extra_vars = extra_vars @@ -100,10 +92,10 @@ class ExtraNamespace(object): def make_ns(self, ns): if self.namespace: - retval = {} - retval.update(self.namespace) - retval.update(ns) - return retval + val = {} + val.update(self.namespace) + val.update(ns) + return val else: return ns diff --git a/tests/test_base.py b/tests/test_base.py index 5e1c23f..6226987 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,5 @@ import os -from pecan import Pecan, expose, request, redirect, abort -from pecan import Pecan, expose, request, redirect, abort +from pecan import Pecan, expose, request, response, redirect, abort from pecan.templating import _builtin_renderers as builtin_renderers from webtest import TestApp from formencode import Schema, validators @@ -305,3 +304,23 @@ class TestEngines(object): r = app.get('/test/1/2/3/4') assert r.status_int == 200 assert r.body == 'it worked' + + def test_streaming_response(self): + class RootController(object): + @expose(content_type='text/plain') + def test(self, foo): + if foo == 'stream': + response.content_type='application/octet-stream' + response.body = 'stream' + return response + else: + return 'plain text' + + app = TestApp(Pecan(RootController())) + r = app.get('/test/stream/') + assert r.content_type == 'application/octet-stream' + assert r.body == 'stream' + + r = app.get('/test/plain/') + assert r.content_type == 'text/plain' + assert r.body == 'plain text'