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
This commit is contained in:
Ryan Petrello
2014-06-25 11:19:43 -04:00
parent 6c7a8b3ee9
commit 21f70bbba7
4 changed files with 95 additions and 16 deletions

View File

@@ -272,8 +272,8 @@ Interacting with the Request and Response Object
For every HTTP request, Pecan maintains a :ref:`thread-local reference
<contextlocals>` 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
----------------------------

View File

@@ -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'
]

View File

@@ -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):

View File

@@ -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):