From 24baeff60cd596acab2464f9d53a15baad000cda Mon Sep 17 00:00:00 2001 From: Chris Petersen Date: Mon, 14 Apr 2014 20:17:33 -0700 Subject: [PATCH] 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 --- falcon/api.py | 20 ++++++++++++++++---- falcon/request.py | 20 ++++++++++++++++++++ tests/test_request_context.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 tests/test_request_context.py diff --git a/falcon/api.py b/falcon/api.py index 7ecf069..a6580b9 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -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) diff --git a/falcon/request.py b/falcon/request.py index ec9de6b..d0d7bf2 100644 --- a/falcon/request.py +++ b/falcon/request.py @@ -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'] diff --git a/tests/test_request_context.py b/tests/test_request_context.py new file mode 100644 index 0000000..242d71d --- /dev/null +++ b/tests/test_request_context.py @@ -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)