From 21f70bbba74ab940b3bf24bfb4cc859fe8926492 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 25 Jun 2014 11:19:43 -0400 Subject: Add support for specifying custom request and response implementations. I noticed that people using pecan have taken to writing custom webob req/resp subclasses and monkeypatching onto `pecan.request` and `pecan.response`. Let's give them what they need to do this properly. Change-Id: If0ac953e381cec3a744388000a3b3afc0ea2525c --- docs/source/routing.rst | 32 ++++++++++++++++++++++++++++++-- pecan/__init__.py | 8 ++++---- pecan/core.py | 32 ++++++++++++++++++++++++-------- pecan/tests/test_base.py | 39 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 16 deletions(-) diff --git a/docs/source/routing.rst b/docs/source/routing.rst index be8e987..68a46ba 100644 --- a/docs/source/routing.rst +++ b/docs/source/routing.rst @@ -272,8 +272,8 @@ Interacting with the Request and Response Object For every HTTP request, Pecan maintains a :ref:`thread-local reference ` to the request and response object, ``pecan.request`` and -``pecan.response``. These are instances of :class:`webob.request.BaseRequest` -and :class:`webob.response.Response`, respectively, and can be interacted with +``pecan.response``. These are instances of :class:`pecan.Request` +and :class:`pecan.Response`, respectively, and can be interacted with from within Pecan controller code:: @pecan.expose() @@ -295,6 +295,34 @@ directly, there may be situations where you want to access them, such as: * Manually rendering a response body +Extending Pecan's Request and Response Object +--------------------------------------------- + +The request and response implementations provided by WebOb are powerful, but +at times, it may be useful to extend application-specific behavior onto your +request and response (such as specialized parsing of request headers or +customized response body serialization). To do so, define custom classes that +inherit from ``pecan.Request`` and ``pecan.Response``, respectively:: + + class MyRequest(pecan.Request): + pass + + class MyResponse(pecan.Response): + pass + +and modify your application configuration to use them:: + + from myproject import MyRequest, MyResponse + + app = { + 'root' : 'project.controllers.root.RootController', + 'modules' : ['project'], + 'static_root' : '%(confdir)s/public', + 'template_path' : '%(confdir)s/project/templates', + 'request_cls': MyRequest, + 'response_cls': MyResponse + } + Mapping Controller Arguments ---------------------------- diff --git a/pecan/__init__.py b/pecan/__init__.py index c294572..4c52713 100644 --- a/pecan/__init__.py +++ b/pecan/__init__.py @@ -1,6 +1,6 @@ from .core import ( - abort, override_template, Pecan, load_app, redirect, render, - request, response + abort, override_template, Pecan, Request, Response, load_app, + redirect, render, request, response ) from .decorators import expose from .hooks import RequestViewerHook @@ -21,8 +21,8 @@ import warnings __all__ = [ - 'make_app', 'load_app', 'Pecan', 'request', 'response', - 'override_template', 'expose', 'conf', 'set_config', 'render', + 'make_app', 'load_app', 'Pecan', 'Request', 'Response', 'request', + 'response', 'override_template', 'expose', 'conf', 'set_config', 'render', 'abort', 'redirect' ] diff --git a/pecan/core.py b/pecan/core.py index 44bad0e..c2f1dab 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -10,7 +10,8 @@ import operator import six -from webob import Request, Response, exc, acceptparse +from webob import (Request as WebObRequest, Response as WebObResponse, exc, + acceptparse) from .compat import urlparse, unquote_plus, izip from .secure import handle_security @@ -37,6 +38,14 @@ class RoutingState(object): self.controller = controller +class Request(WebObRequest): + pass + + +class Response(WebObResponse): + pass + + def proxy(key): class ObjectProxy(object): @@ -120,7 +129,7 @@ def redirect(location=None, internal=False, code=None, headers={}, :param code: The HTTP status code to use for the redirect. Defaults to 302. :param headers: Any HTTP headers to send with the response, as a dictionary. - :param request: The :class:`webob.request.BaseRequest` instance to use. + :param request: The :class:`pecan.Request` instance to use. ''' request = request or state.request @@ -200,11 +209,14 @@ class PecanBase(object): template_path='templates', hooks=lambda: [], custom_renderers={}, extra_template_vars={}, force_canonical=True, guess_content_type_from_ext=True, - context_local_factory=None, **kw): + context_local_factory=None, request_cls=Request, + response_cls=Response, **kw): if isinstance(root, six.string_types): root = self.__translate_root__(root) self.root = root + self.request_cls = request_cls + self.response_cls = response_cls self.renderers = RendererFactory(custom_renderers, extra_template_vars) self.default_renderer = default_renderer @@ -304,7 +316,7 @@ class PecanBase(object): result = getattr(hook, hook_type)(*args) # on_error hooks can choose to return a Response, which will # be used instead of the standard error pages. - if hook_type == 'on_error' and isinstance(result, Response): + if hook_type == 'on_error' and isinstance(result, WebObResponse): return result def get_args(self, state, all_params, remainder, argspec, im_self): @@ -516,7 +528,7 @@ class PecanBase(object): # care of filling it out if result is response: return - elif isinstance(result, Response): + elif isinstance(result, WebObResponse): state.response = result return @@ -567,8 +579,8 @@ class PecanBase(object): ''' # create the request and response object - req = Request(environ) - resp = Response() + req = self.request_cls(environ) + resp = self.response_cls() state = RoutingState(req, resp, self) controller = None @@ -597,7 +609,7 @@ class PecanBase(object): ) # if the on_error handler returned a Response, use it. - if isinstance(on_error_result, Response): + if isinstance(on_error_result, WebObResponse): state.response = on_error_result else: if not isinstance(e, exc.HTTPException): @@ -670,6 +682,10 @@ class Pecan(PecanBase): :param use_context_locals: When `True`, `pecan.request` and `pecan.response` will be available as thread-local references. + :param request_cls: Can be used to specify a custom `pecan.request` object. + Defaults to `pecan.Request`. + :param response_cls: Can be used to specify a custom `pecan.response` + object. Defaults to `pecan.Response`. ''' def __new__(cls, *args, **kw): diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py index 81471ec..89b7a43 100644 --- a/pecan/tests/test_base.py +++ b/pecan/tests/test_base.py @@ -14,8 +14,8 @@ from six import b as b_ from six.moves import cStringIO as StringIO from pecan import ( - Pecan, expose, request, response, redirect, abort, make_app, - override_template, render + Pecan, Request, Response, expose, request, response, redirect, + abort, make_app, override_template, render ) from pecan.templating import ( _builtin_renderers as builtin_renderers, error_formatters @@ -954,6 +954,41 @@ class TestManualResponse(PecanTestCase): assert r.body == b_('Hello, World!') +class TestCustomResponseandRequest(PecanTestCase): + + def test_custom_objects(self): + + class CustomRequest(Request): + + @property + def headers(self): + headers = super(CustomRequest, self).headers + headers['X-Custom-Request'] = 'ABC' + return headers + + class CustomResponse(Response): + + @property + def headers(self): + headers = super(CustomResponse, self).headers + headers['X-Custom-Response'] = 'XYZ' + return headers + + class RootController(object): + @expose() + def index(self): + return request.headers.get('X-Custom-Request') + + app = TestApp(Pecan( + RootController(), + request_cls=CustomRequest, + response_cls=CustomResponse + )) + r = app.get('/') + assert r.body == b_('ABC') + assert r.headers.get('X-Custom-Response') == 'XYZ' + + class TestThreadLocalState(PecanTestCase): def test_thread_local_dir(self): -- cgit v1.2.1