Skip to content

Commit

Permalink
Merge pull request #33 from swistakm/feature/optional-context-handling
Browse files Browse the repository at this point in the history
initial support of optional contexts (refs #32)
  • Loading branch information
swistakm committed Nov 16, 2016
2 parents 9ff2c35 + 826e94f commit dbc2d6f
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ Graceful guide
generic-resources
parameters
serializers
working-with-resources
content-types
documenting-your-api
2 changes: 2 additions & 0 deletions docs/guide/parameters.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _guide-parameters:

Parameters
----------

Expand Down
8 changes: 5 additions & 3 deletions docs/guide/resources.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ your own http GET method handler like following:
.. note::

Due to how falcon works there is **always** only single instance of a
Due to how falcon works there is **always** only a single instance of the
resource class for a single registered route. Please remember to not keep
any state inside of this object (i.e. in ``self``) between any steps of
response generation.
any request processing state inside of this object using ``self.attribute``
lookup. If you need to store and access some additional unique data during
whole request processing flow you may want to use
:ref:`context-aware resource classes <guide-context-aware-resources>`.


2 changes: 2 additions & 0 deletions docs/guide/serializers.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _guide-serializers:

Serializers and fields
----------------------

Expand Down
105 changes: 105 additions & 0 deletions docs/guide/working-with-resources.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
Working with resources
======================

This section of documentation covers various topics related with general
API design handling specific request workflows like:

* Dealing with falcon context object.
* Using hooks and middleware classes.


.. _guide-context-aware-resources:

Dealing with falcon context objects
-----------------------------------


Falcon's ``Request`` object allows you to store some additional context data
under ``Request.context`` attribute in the form of Python dictionary. This
dictionary is available in basic falcon HTTP method handlers like:

* ``on_get(req, resp, **kwargs)``
* ``on_post(req, resp, **kwargs)``
* ``on_put(req, resp, **kwargs)``
* ``on_patch(req, resp, **kwargs)``
* ``on_options(req, resp, **kwargs)``
* ...

Graceful has slighly different design principles. If you use the generic
resource classes (i.e. :any:`RetrieveAPI`, :any:`RetrieveUpdateAPI`,
:any:`ListAPI` and so on) or the :any:`BaseResource` class with
:any:`graceful.resources.mixins` you will usually end up using only the
simple resource modification handlers:

* ``list(params, meta, **kwargs)``
* ``retrieve(params, meta, **kwargs)``
* ``create(params, meta, validated, **kwargs)``
* ...

These handlers do not have the direct access to the request and response
objects (the ``req`` and ``resp`` arguments). In most cases this is not a
problem. Access to the request object is required usually in order to
retrieve client representation of the resource, GET parameters, and headers.
These things should be completely covered with the proper usage of
:ref:`parameter classes <guide-parameters>` and
:ref:`serializer classes <guide-serializers>`. Direct access to the
response object is also rarely required. This is because the serializers are
able to encode resource representation to the response body with negotiated
content-type. If you require additional response access (e.g. to add some
custom response headers), the best way to do that is usually through falcon
middleware classes or hooks.

Anyway, in many cases you may want to work with some unique per-request
context. Typical use cases for that are:

* Providing authentication/authorization objects using middleware classes.
* Providing session/client objects that abstract database connection and
allow handling transactions with automated commits/rollbacks on finished
requests.

Starting from graceful ``0.3.0`` you can define your resource class as a
`context-aware` using ``with_context=True`` keyword argument. This will change
the set of arguments provided to resource manipulation handlers in the generic
API classes:

.. code-block:: python
from graceful.resources.generic import ListAPI
from graceful.serializers import BaseSerializer
class MyListResource(ListAPI, with_context=True)
serializer = BaseSerializer()
def list(self, params, meta, context, **kwargs)
return {}
And in every non-generic resource class that uses mixins:
.. code-block:: python
from graceful.resources.base import BaseResource
from graceful.resources.mixins import ListMixin
class MyListResource(ListMixin, BaseResource, with_context=True):
def list(self, params, meta, context, **kwargs):
pass
The ``context`` argument is exactly the same object as ``Request.context``
that you have access to in your falcon hooks or middleware classes.

.. note::
**Future and backwards compatibility of context-aware resource classes**

Every resource class in graceful ``0.x`` is not context-aware by default.
Starting from ``0.3.0`` the `context-awareness` of the resource
should be explicitly enabled/disabled using the ``with_context`` keyword
argument in class definition. Not doing so will result in ``FutureWarning``
generated on resource class instantiation.

Starting from ``1.0.0`` all resource classes will be `context-aware` by
default and the ``with_context`` keyword argument will become deprecated.
The future of `non-context-aware resources` is still undecided but it is
very likely that they will be removed completely in ``1.x`` branch.
62 changes: 59 additions & 3 deletions src/graceful/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json
import inspect
from collections import OrderedDict
from warnings import warn

from falcon import errors
import falcon
Expand Down Expand Up @@ -64,16 +65,29 @@ def _get_params(mcs, bases, namespace):

return OrderedDict(params)

def __new__(mcs, name, bases, namespace):
def __new__(mcs, name, bases, namespace, **kwargs):
"""Create new class object instance and alter its namespace."""
namespace[mcs._params_storage_key] = mcs._get_params(bases, namespace)

return super().__new__(
# note: there is no need preserve order in namespace anymore so
# we convert it explicitely to dict
mcs, name, bases, dict(namespace)
mcs, name, bases, dict(namespace),
)

def __init__(cls, name, bases, namespace, **kwargs):
"""Perform some additional class object initialization."""
super().__init__(name, bases, namespace)

try:
# note: attribute is stored only if with_context keyword
# argument is not specified explicitely. This way
# we are able to guess if proper warning need to be
# displayed to the user.
# future: remove in 1.x
cls._with_context = kwargs.pop('with_context')
except KeyError:
pass


class BaseResource(metaclass=MetaResource):
"""Base resouce class with core param and response functionality.
Expand All @@ -82,6 +96,25 @@ class BaseResource(metaclass=MetaResource):
and validation of request included representations if serializer is
defined.
All custom resource classes based on ``BaseResource`` accept additional
``with_context`` keyword argument:
.. code-block:: python
class MyResource(BaseResource, with_context=True):
...
The ``with_context`` argument tells if resource modification methods
(metods injected with mixins - list/create/update/etc.) should accept
the ``context`` argument in their signatures. For more details
see :ref:`guide-context-aware-resources` section of documentation. The
default value for ``with_context`` class keyword argument is ``False``.
.. versionadded:: 0.3.0
The ``with_context`` keyword argument.
"""

indent = IntParam(
Expand All @@ -95,6 +128,29 @@ class BaseResource(metaclass=MetaResource):
#: validate resource representations.
serializer = None

def __new__(cls, *args, **kwargs):
"""Do some sanity checks before resource instance initialization."""
instance = super().__new__(cls)

if not hasattr(instance, '_with_context'):
# note: warnings is displayed only if user did not specify
# explicitly that he want's his resource to not accept
# the context keyword argument.
# future: remove in 1.x
warn(
"""
Class {} was defined without the 'with_context' keyword
argument. This means that its resource manipulation
methods (list/retrieve/create etc.) won't receive context
keyword argument.
This behaviour will change in 1.x version of graceful
(please refer to documentation).
""".format(cls),
FutureWarning
)
return instance

@property
def params(self):
"""Return dictionary of parameter definition objects."""
Expand Down
6 changes: 6 additions & 0 deletions src/graceful/resources/mixins.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
from functools import partial

import falcon
from graceful.parameters import IntParam
from graceful.resources.base import BaseResource
Expand Down Expand Up @@ -33,6 +35,10 @@ def handle(self, handler, req, resp, **kwargs):
"""
params = self.require_params(req)

# future: remove in 1.x
if getattr(self, '_with_context', False):
handler = partial(handler, context=req.context)

meta, content = self.require_meta_and_content(
handler, params, **kwargs
)
Expand Down
84 changes: 84 additions & 0 deletions tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
from falcon import errors
import falcon
import pytest
from unittest.mock import Mock

from graceful.errors import ValidationError
from graceful.resources.base import BaseResource
from graceful.resources.generic import Resource
from graceful.resources import mixins
from graceful.parameters import StringParam, BaseParam, IntParam
from graceful.serializers import BaseSerializer
from graceful.fields import StringField
Expand Down Expand Up @@ -458,3 +461,84 @@ def test_require_representation_unsupported_media_type():

with pytest.raises(falcon.HTTPUnsupportedMediaType):
resource.require_representation(Request(env))


@pytest.mark.parametrize("mixin,method,http_handler", [
(mixins.RetrieveMixin, 'retrieve', 'on_get'),
(mixins.ListMixin, 'list', 'on_get'),
(mixins.CreateMixin, 'create', 'on_post'),
(mixins.CreateBulkMixin, 'create_bulk', 'on_patch'),
(mixins.UpdateMixin, 'update', 'on_put'),
])
def test_context_enabled_explicitly(mixin, method, http_handler, req, resp):
# future: remove in 1.x
req.context = {"foo": "bar"}

class ResourceWithContext(mixin, BaseResource, with_context=True):
pass

resource = ResourceWithContext()
mock_method = Mock(return_value={})

setattr(resource, method, mock_method)
getattr(resource, http_handler)(req, resp)

args, kwargs = mock_method.call_args

assert 'context' in kwargs
assert kwargs['context'] == req.context


@pytest.mark.parametrize("mixin,method,http_handler", [
(mixins.RetrieveMixin, 'retrieve', 'on_get'),
(mixins.ListMixin, 'list', 'on_get'),
(mixins.CreateMixin, 'create', 'on_post'),
(mixins.CreateBulkMixin, 'create_bulk', 'on_patch'),
(mixins.UpdateMixin, 'update', 'on_put'),
])
def test_context_disabled_explicitly(mixin, method, http_handler, req, resp):
# future: remove in 1.x
class ResourceWithoutContext(mixin, BaseResource, with_context=False):
pass

resource = ResourceWithoutContext()
mock_method = Mock(return_value={})

setattr(resource, method, mock_method)
getattr(resource, http_handler)(req, resp)

args, kwargs = mock_method.call_args

assert 'context' not in kwargs


@pytest.mark.parametrize("mixin,method,http_handler", [
(mixins.RetrieveMixin, 'retrieve', 'on_get'),
(mixins.ListMixin, 'list', 'on_get'),
(mixins.CreateMixin, 'create', 'on_post'),
(mixins.CreateBulkMixin, 'create_bulk', 'on_patch'),
(mixins.UpdateMixin, 'update', 'on_put'),
])
def test_context_disabled_implicitly(mixin, method, http_handler, req, resp):
# future: remove in 1.x
class ResourceWithoutContext(mixin, BaseResource):
pass

resource = ResourceWithoutContext()
mock_method = Mock(return_value={})

setattr(resource, method, mock_method)
getattr(resource, http_handler)(req, resp)

args, kwargs = mock_method.call_args

assert 'context' not in kwargs


def test_warns_about_context_disabled_implicitly():
# future: remove in 1.x
class ResourceWithoutContext(BaseResource):
pass

with pytest.warns(FutureWarning):
ResourceWithoutContext()

0 comments on commit dbc2d6f

Please sign in to comment.