feat(API): Support custom Request and Response classes

This patch adds support for supplying custom request and
response classes.

As part of this work, a new "context" attribute was added
to Request, and its type may be overridden by setting the
"context_type" class attribute in a custom Request class.

The alternative to using a class attribute would have been
to pass the context type into the api, and then each time
a class was instantiated, pass the type into it's
initializer. After discussion on IRC, it was decided that
using a class attribute would be more elegant.

See commentary at: https://github.com/racker/falcon/pull/256

Closes #256 and #248
This commit is contained in:
Chris Petersen
2014-04-14 20:17:33 -07:00
committed by kgriffs
parent 6f3b608363
commit 24baeff60c
3 changed files with 69 additions and 4 deletions

View File

@@ -42,13 +42,22 @@ class API(object):
after (callable, optional): A global action hook (or list of hooks)
to call after each on_* responder, for all resources. Similar to
the ``after`` decorator, but applies to the entire API.
request_type (Request, optional): Request-alike class to use instead
of Falcon's default class. Useful if you wish to extend
``falcon.request.Request`` with a custom ``context_type``.
(default falcon.request.Request)
response_type (Response, optional): Response-alike class to use
instead of Falcon's default class. (default
falcon.response.Response)
"""
__slots__ = ('_after', '_before', '_error_handlers', '_media_type',
__slots__ = ('_after', '_before', '_request_type', '_response_type',
'_error_handlers', '_media_type',
'_routes', '_default_route', '_sinks')
def __init__(self, media_type=DEFAULT_MEDIA_TYPE, before=None, after=None):
def __init__(self, media_type=DEFAULT_MEDIA_TYPE, before=None, after=None,
request_type=Request, response_type=Response):
self._routes = []
self._sinks = []
self._default_route = None
@@ -57,6 +66,9 @@ class API(object):
self._before = helpers.prepare_global_hooks(before)
self._after = helpers.prepare_global_hooks(after)
self._request_type = request_type
self._response_type = response_type
self._error_handlers = []
def __call__(self, env, start_response):
@@ -75,8 +87,8 @@ class API(object):
"""
req = Request(env)
resp = Response()
req = self._request_type(env)
resp = self._response_type()
responder, params = self._get_responder(
req.path, req.method)

View File

@@ -61,6 +61,14 @@ class Request(object):
hosting).
env (dict): Reference to the WSGI *environ* dict passed in from the
server. See also PEP-3333.
context (dict): Dictionary to hold any data about the request which is
specific to your app (e.g. session object). Falcon itself will
not interact with this attribute after it has been initialized.
context_type (None): Custom callable/type to use for initializing the
``context`` attribute. To change this value so that ``context``
is initialized to the type of your choice (e.g. OrderedDict), you
will need to extend this class and pass that new type to the
``request_type`` argument of ``falcon.API()``.
uri (str): The fully-qualified URI for the request.
url (str): alias for ``uri``.
relative_uri (str): The path + query string portion of the full URI.
@@ -137,12 +145,24 @@ class Request(object):
'path',
'query_string',
'stream',
'context',
'_wsgierrors',
)
# Allow child classes to override this
context_type = None
def __init__(self, env):
self.env = env
if self.context_type is None:
# Literal syntax is more efficient than using dict()
self.context = {}
else:
# pylint will detect this as not-callable because it only sees the
# declaration of None, not whatever type a subclass may have set.
self.context = self.context_type() # pylint: disable=not-callable
self._wsgierrors = env['wsgi.errors']
self.stream = env['wsgi.input']
self.method = env['REQUEST_METHOD']

View File

@@ -0,0 +1,33 @@
import falcon.testing as testing
from falcon.request import Request
class TestRequestContext(testing.TestBase):
def test_default_request_context(self):
env = testing.create_environ()
req = Request(env)
self.assertIsInstance(req.context, dict)
def test_custom_request_context(self):
# Define a Request-alike with a custom context type
class MyCustomContextType():
pass
class MyCustomRequest(Request):
context_type = MyCustomContextType
env = testing.create_environ()
req = MyCustomRequest(env)
self.assertIsInstance(req.context, MyCustomContextType)
def test_custom_request_context_failure(self):
# Define a Request-alike with a non-callable custom context type
class MyCustomRequest(Request):
context_type = False
env = testing.create_environ()
self.assertRaises(TypeError, MyCustomRequest, env)