Repository: falconry/falcon
Pull request #1408 (2019-01-06 18:32:46)

feat(request, response): change context type to a subclass of dict
ref https://github.com/falconry/falcon/issues/1120

Work in progress:

- [x] Make ``Request`` / ``Response`` use ``context_type`` subclassing dict
- [x] Update unit tests
- [x] Update FAQ
- [x] Update benchmark code
- [x] Update examples in ``README.rst`` and elsewhere
- [x] ~~Resolve Hug tests (Hug is relying on dict context)~~ :arrow_left: not any longer an issue with dict subclass approach

codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=h1) Report
> Merging [#1408](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/280faa024b8c09978fbc9c86dcea202fe5b9e214?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1408/graphs/tree.svg?width=650&token=zOtkfNQ3h6&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1408   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          38      38           
  Lines        2550    2550           
  Branches      393     393           
======================================
  Hits         2550    2550
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/request.py](https://codecov.io/gh/falconry/falcon/pull/1408/diff?src=pr&el=tree#diff-ZmFsY29uL3JlcXVlc3QucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=footer). Last update [280faa0...4f03416](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=h1) Report
> Merging [#1408](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/280faa024b8c09978fbc9c86dcea202fe5b9e214?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1408/graphs/tree.svg?width=650&token=zOtkfNQ3h6&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1408   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          38      38           
  Lines        2550    2550           
  Branches      393     393           
======================================
  Hits         2550    2550
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/request.py](https://codecov.io/gh/falconry/falcon/pull/1408/diff?src=pr&el=tree#diff-ZmFsY29uL3JlcXVlc3QucHk=) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=footer). Last update [280faa0...4f03416](https://codecov.io/gh/falconry/falcon/pull/1408?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

vytas7: @nZac I think I found a semi-elegant way to do that, but I think that would bog down users with warnings being spammed upon every request provided they use dict context. So I think it is best to go forward with this now and give users some breathing space to adapt to the new changes, and start emitting these warnings in a later release.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 49afe0a..b5572f2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,8 @@ Breaking Changes
   ``False``.
 - ``RequestOptions.auto_parse_qs_csv`` now defaults to ``False`` instead of
   ``True``.
+- ``Request.context_type`` was changed from dict to a subclass of dict.
+- ``Response.context_type`` was changed from dict to a subclass of dict.
 - ``JSONHandler`` and ``HTTPError`` no longer use
   `ujson` in lieu of the standard `json` library (when `ujson` is available in
   the environment). Instead, ``JSONHandler`` can now be configured
@@ -31,6 +33,36 @@ New & Improved
 
 - Added a new ``headers`` property to the ``Response`` class.
 - Removed ``six`` as a dependency.
+- ``Request.context_type`` now defaults to a bare class allowing to set
+  attributes on the request context object::
+
+    # Before
+    req.context['role'] = 'trial'
+    req.context['user'] = 'guest'
+
+    # Falcon 2.0
+    req.context.role = 'trial'
+    req.context.user = 'guest'
+
+  To ease the migration path, the previous behavior is supported by subclassing
+  dict, however, as of Falcon 2.0, the dict context interface is considered
+  deprecated, and may be removed in a future release. It is also noteworthy
+  that object attributes and dict items are not automagically linked in any
+  special way, and setting one does not affect the other.
+- ``Response.context_type`` now defaults to a bare class allowing
+  to set attributes on the response context object::
+
+    # Before
+    resp.context['cache_strategy'] = 'lru'
+
+    # Falcon 2.0
+    resp.context.cache_strategy = 'lru'
+
+  To ease the migration path, the previous behavior is supported by subclassing
+  dict, however, as of Falcon 2.0, the dict context interface is considered
+  deprecated, and may be removed in a future release. It is also noteworthy
+  that object attributes and dict items are not automagically linked in any
+  special way, and setting one does not affect the other.
 - ``JSONHandler`` can now be configured to use arbitrary
   ``dumps()`` and ``loads()`` functions. This enables support not only for
   using any of a number of third-party JSON libraries, but also for
diff --git a/README.rst b/README.rst
index 415ee42..0ce4a6f 100644
--- a/README.rst
+++ b/README.rst
@@ -532,7 +532,7 @@ bodies.
                                             'A valid JSON document is required.')
 
             try:
-                req.context['doc'] = json.loads(body.decode('utf-8'))
+                req.context.doc = json.loads(body.decode('utf-8'))
 
             except (ValueError, UnicodeDecodeError):
                 raise falcon.HTTPError(falcon.HTTP_753,
@@ -542,10 +542,10 @@ bodies.
                                        'UTF-8.')
 
         def process_response(self, req, resp, resource):
-            if 'result' not in resp.context:
+            if not hasattr(resp.context, 'result'):
                 return
 
-            resp.body = json.dumps(resp.context['result'])
+            resp.body = json.dumps(resp.context.result)
 
 
     def max_body(limit):
@@ -593,7 +593,7 @@ bodies.
             #
             # NOTE: Starting with Falcon 1.3, you can simply
             # use resp.media for this instead.
-            resp.context['result'] = result
+            resp.context.result = result
 
             resp.set_header('Powered-By', 'Falcon')
             resp.status = falcon.HTTP_200
@@ -603,8 +603,8 @@ bodies.
             try:
                 # NOTE: Starting with Falcon 1.3, you can simply
                 # use req.media for this instead.
-                doc = req.context['doc']
-            except KeyError:
+                doc = req.context.doc
+            except AttributeError:
                 raise falcon.HTTPBadRequest(
                     'Missing thing',
                     'A thing must be submitted in the request body.')
diff --git a/docs/api/hooks.rst b/docs/api/hooks.rst
index dfafabe..f3eff3a 100644
--- a/docs/api/hooks.rst
+++ b/docs/api/hooks.rst
@@ -86,9 +86,9 @@ requests.
 
 .. Tip::
     In order to pass data from a hook function to a resource function
-    use the ``req.context`` and ``resp.context`` dictionaries. These context
-    dictionaries are intended to hold request and response data specific to
-    your app as it passes through the framework.
+    use the ``req.context`` and ``resp.context`` objects. These context objects
+    are intended to hold request and response data specific to your app as it
+    passes through the framework.
 
 .. automodule:: falcon
     :members: before, after
diff --git a/docs/api/middleware.rst b/docs/api/middleware.rst
index 72627af..69eac91 100644
--- a/docs/api/middleware.rst
+++ b/docs/api/middleware.rst
@@ -73,9 +73,9 @@ Falcon's middleware interface is defined as follows:
 
 .. Tip::
     In order to pass data from a middleware function to a resource function
-    use the ``req.context`` and ``resp.context`` dictionaries. These context
-    dictionaries are intended to hold request and response data specific to
-    your app as it passes through the framework.
+    use the ``req.context`` and ``resp.context`` objects. These context objects
+    are intended to hold request and response data specific to your app as it
+    passes through the framework.
 
 Each component's *process_request*, *process_resource*, and
 *process_response* methods are executed hierarchically, as a stack, following
diff --git a/docs/changes/2.0.0.rst b/docs/changes/2.0.0.rst
index ac98286..e6d43fd 100644
--- a/docs/changes/2.0.0.rst
+++ b/docs/changes/2.0.0.rst
@@ -16,6 +16,10 @@ Breaking Changes
   instead of ``False``.
 - :attr:`~.RequestOptions.auto_parse_qs_csv` now defaults to ``False``
   instead of ``True``.
+- Request :attr:`~.Request.context_type` was changed from dict to a subclass of
+  dict.
+- Response :attr:`~.Response.context_type` was changed from dict to a subclass
+  of dict.
 - :class:`~.media.JSONHandler` and :class:`~.HTTPError` no longer use
   `ujson` in lieu of the standard `json` library (when `ujson` is available in
   the environment). Instead, :class:`~.media.JSONHandler` can now be configured
@@ -31,6 +35,43 @@ New & Improved
 
 - Added a new :attr:`~.Response.headers` property to the :class:`~.Response` class.
 - Removed :py:mod:`six` from deps.
+- Request :attr:`~.Request.context_type` now defaults to a bare class allowing
+  to set attributes on the request context object::
+
+    # Before
+    req.context['role'] = 'trial'
+    req.context['user'] = 'guest'
+
+    # Falcon 2.0
+    req.context.role = 'trial'
+    req.context.user = 'guest'
+
+  To ease the migration path, the previous behavior is supported by subclassing
+  dict, however, as of Falcon 2.0, the dict context interface is considered
+  deprecated, and may be removed in a future release. It is also noteworthy
+  that object attributes and dict items are not automagically linked in any
+  special way, and setting one does not affect the other.
+
+  Applications can work around this change by explicitly overriding
+  :attr:`~.Request.context_type` to dict.
+
+- Response :attr:`~.Response.context_type` now defaults to a bare class allowing
+  to set attributes on the response context object::
+
+    # Before
+    resp.context['cache_strategy'] = 'lru'
+
+    # Falcon 2.0
+    resp.context.cache_strategy = 'lru'
+
+  To ease the migration path, the previous behavior is supported by subclassing
+  dict, however, as of Falcon 2.0, the dict context interface is considered
+  deprecated, and may be removed in a future release. It is also noteworthy
+  that object attributes and dict items are not automagically linked in any
+  special way, and setting one does not affect the other.
+
+  Applications can work around this change by explicitly overriding
+  :attr:`~.Response.context_type` to dict.
 - :class:`~.media.JSONHandler` can now be configured to use arbitrary
   ``dumps()`` and ``loads()`` functions. This enables support not only for
   using any of a number of third-party JSON libraries, but also for
diff --git a/docs/user/faq.rst b/docs/user/faq.rst
index 4c8c8f0..55ef2d4 100644
--- a/docs/user/faq.rst
+++ b/docs/user/faq.rst
@@ -58,7 +58,7 @@ implementation-first approach), while other developers prefer to use the API
 spec itself as the contract, implementing and testing the API against that spec
 (taking a design-first approach).
 
-At the risk of erring on the side of flexiblity, Falcon does not provide API
+At the risk of erring on the side of flexibility, Falcon does not provide API
 spec support out of the box. However, there are several community projects
 available in this vein. Our
 `Add on Catalog <https://github.com/falconry/falcon/wiki/Add-on-Catalog>`_ lists
@@ -177,7 +177,7 @@ For more sophisticated use cases, have a look at Falcon add-ons from the
 community, such as `falcon-cors <https://github.com/lwcolton/falcon-cors>`_, or
 try one of the generic
 `WSGI CORS libraries available on PyPI <https://pypi.python.org/pypi?%3Aaction=search&term=cors&submit=search>`_.
-If you use an API gateway, you might also look into what CORS functionaly
+If you use an API gateway, you might also look into what CORS functionality
 it provides at that level.
 
 How do I implement redirects within Falcon?
@@ -416,8 +416,26 @@ See also the `WSGI middleware example <https://www.python.org/dev/peps/pep-3333/
 How can I pass data from a hook to a responder, and between hooks?
 ------------------------------------------------------------------
 You can inject extra responder kwargs from a hook by adding them
-to the *params* dict passed into the hook. You can also add custom data to
-the ``req.context`` dict, as a way of passing contextual information around.
+to the *params* dict passed into the hook. You can also set custom attributes
+on the ``req.context`` object, as a way of passing contextual information
+around:
+
+.. code:: python
+
+    def authorize(req, resp, resource, params):
+        # Check authentication/authorization
+        # ...
+
+        req.context.role = 'root'
+        req.context.scopes = ('storage', 'things')
+        req.context.uid = 0
+
+    # ...
+
+    @falcon.before(authorize)
+    def on_post(self, req, resp):
+        pass
+
 
 How can I write a custom handler for 404 and 500 pages in falcon?
 ------------------------------------------------------------------
@@ -584,6 +602,52 @@ In the meantime, the workaround is to percent-encode the forward slash. If you
 don’t control the clients and can't enforce this, you can implement a Falcon
 middleware component to rewrite the path before it is routed.
 
+How do I adapt my code to default context type changes in Falcon 2.0?
+---------------------------------------------------------------------
+
+The default request/response context type has been changed from dict to a class
+subclassing dict in Falcon 2.0. Instead of setting dictionary items, you can
+now simply set attributes on the object:
+
+.. code:: python
+
+   # Before Falcon 2.0
+   req.context['cache_backend'] = MyUltraFastCache.connect()
+
+   # Falcon 2.0
+   req.context.cache_backend = MyUltraFastCache.connect()
+
+Since the default context type derives from dict, the existing code will work
+unmodified with Falcon 2.0. Nevertheless, it is recommended to change usage
+as outlined above since the ability to use context as dictionary may be
+removed in a future release.
+
+.. warning::
+   Context attributes are not linked to dict items in any special way,
+   i.e. setting an object attribute does not set the corresponding dict item,
+   nor vice versa.
+
+   Furthermore, if you need to mix-and-match both approaches under migration,
+   beware that setting attributes such as *items* or *values* would obviously
+   shadow the corresponding dict functions.
+
+If an existing project is making extensive use of dictionary contexts, the type
+can be explicitly overridden back to dict by employing custom request/response
+types:
+
+.. code:: python
+
+    class RequestWithDictContext(falcon.Request):
+        context_type = dict
+
+    class ResponseWithDictContext(falcon.Response):
+        context_type = dict
+
+    # ...
+
+    api = falcon.API(request_type=RequestWithDictContext,
+                     response_type=ResponseWithDictContext)
+
 Response Handling
 ~~~~~~~~~~~~~~~~~
 
diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst
index 4f95b67..271c8ca 100644
--- a/docs/user/quickstart.rst
+++ b/docs/user/quickstart.rst
@@ -193,7 +193,7 @@ parameters, handling errors, and working with request and response bodies.
                                             'A valid JSON document is required.')
 
             try:
-                req.context['doc'] = json.loads(body.decode('utf-8'))
+                req.context.doc = json.loads(body.decode('utf-8'))
 
             except (ValueError, UnicodeDecodeError):
                 raise falcon.HTTPError(falcon.HTTP_753,
@@ -203,10 +203,10 @@ parameters, handling errors, and working with request and response bodies.
                                        'UTF-8.')
 
         def process_response(self, req, resp, resource):
-            if 'result' not in resp.context:
+            if not hasattr(resp.context, 'result'):
                 return
 
-            resp.body = json.dumps(resp.context['result'])
+            resp.body = json.dumps(resp.context.result)
 
 
     def max_body(limit):
@@ -254,7 +254,7 @@ parameters, handling errors, and working with request and response bodies.
             #
             # NOTE: Starting with Falcon 1.3, you can simply 
             # use resp.media for this instead.
-            resp.context['result'] = result
+            resp.context.result = result
 
             resp.set_header('Powered-By', 'Falcon')
             resp.status = falcon.HTTP_200
@@ -262,8 +262,8 @@ parameters, handling errors, and working with request and response bodies.
         @falcon.before(max_body(64 * 1024))
         def on_post(self, req, resp, user_id):
             try:
-                doc = req.context['doc']
-            except KeyError:
+                doc = req.context.doc
+            except AttributeError:
                 raise falcon.HTTPBadRequest(
                     'Missing thing',
                     'A thing must be submitted in the request body.')
diff --git a/falcon/bench/queues/api.py b/falcon/bench/queues/api.py
index da7a804..ee7ba42 100644
--- a/falcon/bench/queues/api.py
+++ b/falcon/bench/queues/api.py
@@ -22,10 +22,10 @@ from falcon.bench.queues import stats
 
 class RequestIDComponent(object):
     def process_request(self, req, resp):
-        req.context['request_id'] = '<generate ID>'
+        req.context.request_id = '<generate ID>'
 
     def process_response(self, req, resp, resource):
-        resp.set_header('X-Request-ID', req.context['request_id'])
+        resp.set_header('X-Request-ID', req.context.request_id)
 
 
 class CannedResponseComponent(object):
diff --git a/falcon/request.py b/falcon/request.py
index 660115c..a21406c 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -75,15 +75,27 @@ class Request(object):
     Attributes:
         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 (object): Empty object to hold any data (in its attributes)
+            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.
+
+            Note:
+                **New in 2.0:** the default `context_type` (see below) was
+                changed from dict to a bare class, and the preferred way to
+                pass request-specific data is now to set attributes directly on
+                the `context` object, for example::
+
+                    req.context.role = 'trial'
+                    req.context.user = 'guest'
+
         context_type (class): Class variable that determines the factory or
             type to use for initializing the `context` attribute. By default,
-            the framework will instantiate standard ``dict`` objects. However,
-            you may override this behavior by creating a custom child class of
-            ``falcon.Request``, and then passing that new class to
-            `falcon.API()` by way of the latter's `request_type` parameter.
+            the framework will instantiate bare objects (instances of the bare
+            RequestContext class). However, you may override this behavior by
+            creating a custom child class of ``falcon.Request``, and then
+            passing that new class to `falcon.API()` by way of the latter's
+            `request_type` parameter.
 
             Note:
                 When overriding `context_type` with a factory function (as
@@ -412,7 +424,7 @@ class Request(object):
     )
 
     # Child classes may override this
-    context_type = dict
+    context_type = type('RequestContext', (dict,), {})
 
     _wsgi_input_type_known = False
     _always_wrap_wsgi_input = False
diff --git a/falcon/response.py b/falcon/response.py
index a213773..373737e 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -108,15 +108,26 @@ class Response(object):
 
         stream_len (int): Deprecated alias for :py:attr:`content_length`.
 
-        context (dict): Dictionary to hold any data about the response which is
-            specific to your app. Falcon itself will not interact with this
-            attribute after it has been initialized.
+        context (object): Empty object to hold any data (in its attributes)
+            about the response which is specific to your app (e.g. session
+            object). Falcon itself will not interact with this attribute after
+            it has been initialized.
+
+            Note:
+                **New in 2.0:** the default `context_type` (see below) was
+                changed from dict to a bare class, and the preferred way to
+                pass response-specific data is now to set attributes directly
+                on the `context` object, for example::
+
+                    resp.context.cache_strategy = 'lru'
+
         context_type (class): Class variable that determines the factory or
             type to use for initializing the `context` attribute. By default,
-            the framework will instantiate standard ``dict`` objects. However,
-            you may override this behavior by creating a custom child class of
-            ``falcon.Response``, and then passing that new class to
-            `falcon.API()` by way of the latter's `response_type` parameter.
+            the framework will instantiate bare objects (instances of the bare
+            ResponseContext class). However, you may override this behavior by
+            creating a custom child class of ``falcon.Response``, and then
+            passing that new class to `falcon.API()` by way of the latter's
+            `response_type` parameter.
 
             Note:
                 When overriding `context_type` with a factory function (as
@@ -146,7 +157,7 @@ class Response(object):
     )
 
     # Child classes may override this
-    context_type = dict
+    context_type = type('ResponseContext', (dict,), {})
 
     def __init__(self, options=None):
         self.status = '200 OK'


Test patch:
diff --git a/tests/test_example.py b/tests/test_example.py
index fa2e888..7c35340 100644
--- a/tests/test_example.py
+++ b/tests/test_example.py
@@ -109,7 +109,7 @@ class JSONTranslator(object):
                                         'A valid JSON document is required.')
 
         try:
-            req.context['doc'] = json.loads(body.decode('utf-8'))
+            req.context.doc = json.loads(body.decode('utf-8'))
 
         except (ValueError, UnicodeDecodeError):
             raise falcon.HTTPError(falcon.HTTP_753,
@@ -119,10 +119,10 @@ class JSONTranslator(object):
                                    'UTF-8.')
 
     def process_response(self, req, resp, resource):
-        if 'result' not in req.context:
+        if not hasattr(req.context, 'result'):
             return
 
-        resp.body = json.dumps(req.context['result'])
+        resp.body = json.dumps(req.context.result)
 
 
 def max_body(limit):
@@ -167,7 +167,7 @@ class ThingsResource(object):
         # create a custom class that inherits from falcon.Request. This
         # class could, for example, have an additional 'doc' property
         # that would serialize to JSON under the covers.
-        req.context['result'] = result
+        req.context.result = result
 
         resp.set_header('Powered-By', 'Falcon')
         resp.status = falcon.HTTP_200
@@ -175,8 +175,8 @@ class ThingsResource(object):
     @falcon.before(max_body(64 * 1024))
     def on_post(self, req, resp, user_id):
         try:
-            doc = req.context['doc']
-        except KeyError:
+            doc = req.context.doc
+        except AttributeError:
             raise falcon.HTTPBadRequest(
                 'Missing thing',
                 'A thing must be submitted in the request body.')
diff --git a/tests/test_request_context.py b/tests/test_request_context.py
index 9336c9a..cc61f86 100644
--- a/tests/test_request_context.py
+++ b/tests/test_request_context.py
@@ -9,7 +9,15 @@ class TestRequestContext(object):
     def test_default_request_context(self):
         env = testing.create_environ()
         req = Request(env)
-        assert isinstance(req.context, dict)
+        assert type(req.context).__name__ == 'RequestContext'
+
+        req.context.hello = 'World'
+        assert req.context.hello == 'World'
+        assert 'hello' not in req.context
+
+        req.context['note'] = 'Default Request.context_type used to be dict.'
+        assert 'note' in req.context
+        assert req.context.get('note') == req.context['note']
 
     def test_custom_request_context(self):
 
diff --git a/tests/test_response_context.py b/tests/test_response_context.py
index 87787bb..2790e6f 100644
--- a/tests/test_response_context.py
+++ b/tests/test_response_context.py
@@ -3,11 +3,19 @@ import pytest
 from falcon import Response
 
 
-class TestRequestContext(object):
+class TestResponseContext(object):
 
     def test_default_response_context(self):
         resp = Response()
-        assert isinstance(resp.context, dict)
+        assert type(resp.context).__name__ == 'ResponseContext'
+
+        resp.context.hello = 'World!'
+        assert resp.context.hello == 'World!'
+        assert 'hello' not in resp.context
+
+        resp.context['note'] = 'Default Response.context_type used to be dict.'
+        assert 'note' in resp.context
+        assert resp.context.get('note') == resp.context['note']
 
     def test_custom_response_context(self):