Repository: falconry/falcon
Pull request #1486 (2019-03-27 19:45:07)

feat: improve dict-compatibility for the default Request and Response context type

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

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

```diff
@@          Coverage Diff           @@
##           master   #1486   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          39      39           
  Lines        2613    2673   +60     
  Branches      416     416           
======================================
+ Hits         2613    2673   +60
```


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

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1486?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/1486?src=pr&el=footer). Last update [54e227d...cd4edd3](https://codecov.io/gh/falconry/falcon/pull/1486?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 310280d..bd9176f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -67,8 +67,10 @@ Breaking Changes
 - The deprecated ``stream_len`` property was removed from the ``Response``
   class. Please use ``Response.set_stream()`` or ``Response.content_length``
   instead.
-- ``Request.context_type`` was changed from dict to a subclass of dict.
-- ``Response.context_type`` was changed from dict to a subclass of dict.
+- ``Request.context_type`` was changed from dict to a bare class implementing
+  the mapping interface.
+- ``Response.context_type`` was changed from dict to a bare class implementing
+  the mapping interface.
 - ``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
@@ -224,11 +226,11 @@ New & Improved
     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.
+  To ease the migration path, the previous behavior is supported by
+  implementing the mapping interface in a way that object attributes and
+  mapping items are linked, and setting one sets the other as well. However, as
+  of Falcon 2.0, the dict context interface is considered deprecated, and may
+  be removed in a future release.
 - ``Response.context_type`` now defaults to a bare class allowing
   to set attributes on the response context object::
 
@@ -238,11 +240,11 @@ New & Improved
     # 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.
+  To ease the migration path, the previous behavior is supported by
+  implementing the mapping interface in a way that object attributes and
+  mapping items are linked, and setting one sets the other as well. However, as
+  of Falcon 2.0, the dict context interface is considered deprecated, and may
+  be removed in a future release.
 - ``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/api/util.rst b/docs/api/util.rst
index f7e9771..fa8437d 100644
--- a/docs/api/util.rst
+++ b/docs/api/util.rst
@@ -20,5 +20,9 @@ Miscellaneous
 .. autoclass:: falcon.TimezoneGMT
     :members:
 
+.. autoclass:: falcon.Context
+    :members:
+    :no-undoc-members:
+
 .. autoclass:: falcon.ETag
     :members:
diff --git a/docs/changes/2.0.0.rst b/docs/changes/2.0.0.rst
index 0869490..3c965db 100644
--- a/docs/changes/2.0.0.rst
+++ b/docs/changes/2.0.0.rst
@@ -69,10 +69,12 @@ Breaking Changes
   be an alias of the new :attr:`~.Response.content_length` property. Please
   use :meth:`~.Response.set_stream` or :attr:`~.Response.content_length`
   instead, going forward, as ``stream_len`` is now deprecated.
-- 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.
+- Request :attr:`~.Request.context_type` was changed from dict to a bare class
+  implementing the mapping interface.
+  (See also: :ref:`bare_class_context_type`)
+- Response :attr:`~.Response.context_type` was changed from dict to a bare class
+  implementing the mapping interface.
+  (See also: :ref:`bare_class_context_type`)
 - :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
@@ -232,15 +234,15 @@ New & Improved
     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.
+  To ease the migration path, the previous behavior is supported by
+  implementing the mapping interface in a way that object attributes and
+  mapping items are linked, and setting one sets the other as well. However, as
+  of Falcon 2.0, the dict context interface is considered deprecated, and may
+  be removed in a future release.
 
   Applications can work around this change by explicitly overriding
   :attr:`~.Request.context_type` to dict.
-
+  (See also: :ref:`bare_class_context_type`)
 - Response :attr:`~.Response.context_type` now defaults to a bare class allowing
   to set attributes on the response context object::
 
@@ -250,14 +252,15 @@ New & Improved
     # 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.
+  To ease the migration path, the previous behavior is supported by
+  implementing the mapping interface in a way that object attributes and
+  mapping items are linked, and setting one sets the other as well. However, as
+  of Falcon 2.0, the dict context interface is considered deprecated, and may
+  be removed in a future release.
 
   Applications can work around this change by explicitly overriding
   :attr:`~.Response.context_type` to dict.
+  (See also: :ref:`bare_class_context_type`)
 - :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 55ef2d4..338fa29 100644
--- a/docs/user/faq.rst
+++ b/docs/user/faq.rst
@@ -602,12 +602,14 @@ 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.
 
+.. _bare_class_context_type:
+
 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:
+The default request/response context type has been changed from dict to a bare
+class in Falcon 2.0. Instead of setting dictionary items, you can now simply
+set attributes on the object:
 
 .. code:: python
 
@@ -617,19 +619,18 @@ now simply set attributes on the object:
    # 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.
+The new default context type emulates a dict-like mapping interface in a way
+that context attributes are linked to dict items, i.e. setting an object
+attribute also sets the corresponding dict item, and vice versa. As a result,
+existing code will largely work unmodified with Falcon 2.0. Nevertheless, it is
+recommended to migrate to the new interface as outlined above since the
+dict-like mapping interface may be removed from the context type 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 you need to mix-and-match both approaches under migration, beware that
+   setting attributes such as *items* or *values* would obviously shadow the
+   corresponding mapping interface 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
diff --git a/falcon/request.py b/falcon/request.py
index 4ddcf54..b3910de 100644
--- a/falcon/request.py
+++ b/falcon/request.py
@@ -24,6 +24,7 @@ from falcon.forwarded import Forwarded  # NOQA
 from falcon.media import Handlers
 from falcon.util import compat
 from falcon.util import json
+from falcon.util import structures
 from falcon.util.uri import parse_host, parse_query_string
 from falcon.vendor import mimeparse
 
@@ -72,10 +73,10 @@ class Request(object):
         context_type (class): Class variable that determines the factory or
             type to use for initializing the `context` attribute. By default,
             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.
+            :class:`falcon.Context` 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
@@ -429,7 +430,7 @@ class Request(object):
     _cached_if_none_match = None
 
     # Child classes may override this
-    context_type = type('RequestContext', (dict,), {})
+    context_type = structures.Context
 
     _wsgi_input_type_known = False
 
diff --git a/falcon/response.py b/falcon/response.py
index eb25ce4..cb84fae 100644
--- a/falcon/response.py
+++ b/falcon/response.py
@@ -28,7 +28,7 @@ from falcon.response_helpers import (
     header_property,
     is_ascii_encodable,
 )
-from falcon.util import compat, dt_to_http, TimezoneGMT
+from falcon.util import compat, dt_to_http, structures, TimezoneGMT
 from falcon.util.uri import encode as uri_encode
 from falcon.util.uri import encode_value as uri_encode_value
 
@@ -135,10 +135,10 @@ class Response(object):
         context_type (class): Class variable that determines the factory or
             type to use for initializing the `context` attribute. By default,
             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.
+            :class:`falcon.Context` 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
@@ -174,7 +174,7 @@ class Response(object):
     complete = False
 
     # Child classes may override this
-    context_type = type('ResponseContext', (dict,), {})
+    context_type = structures.Context
 
     def __init__(self, options=None):
         self.status = '200 OK'
diff --git a/falcon/util/structures.py b/falcon/util/structures.py
index cba3e77..666f467 100644
--- a/falcon/util/structures.py
+++ b/falcon/util/structures.py
@@ -107,6 +107,134 @@ class CaseInsensitiveDict(MutableMapping):  # pragma: no cover
         return '%s(%r)' % (self.__class__.__name__, dict(self.items()))
 
 
+# NOTE(vytas): Although Context is effectively implementing the MutableMapping
+#   interface, we choose not to subclass MutableMapping to stress the fact that
+#   Context is, by design, a bare class, and the mapping interface may be
+#   removed in a future Falcon release.
+class Context(object):
+    """
+    Convenience class to hold contextual information in its attributes.
+
+    This class is used as the default :class:`~.Request` and :class:`~Response`
+    context type (see
+    :attr:`Request.context_type <falcon.Request.context_type>` and
+    :attr:`Response.context_type <falcon.Response.context_type>`,
+    respectively).
+
+    In Falcon versions prior to 2.0, the default context type was ``dict``. To
+    ease the migration to attribute-based context object approach, this class
+    also implements the mapping interface; that is, object attributes are
+    linked to dictionary items, and vice versa. For instance:
+
+    >>> context = falcon.Context()
+    >>> context.cache_strategy = 'lru'
+    >>> context.get('cache_strategy')
+    'lru'
+    >>> 'cache_strategy' in context
+    True
+
+    Note:
+        Python 2 specific ``dict`` methods are exposed regardless of the
+        Python language version, however, as they are delegated to the
+        underlying ``__dict__``, a similar error would be raised as if
+        attempting to use these methods for a usual Python 3 dict.
+    """
+
+    def __contains__(self, key):
+        return self.__dict__.__contains__(key)
+
+    def __getitem__(self, key):
+        # PERF(vytas): On CPython, using this mapping interface (instead of a
+        #   standard dict) to get, set and delete items incurs overhead
+        #   approximately comparable to that of two function calls
+        #   (per get/set/delete operation, that is).
+        return self.__dict__.__getitem__(key)
+
+    def __setitem__(self, key, value):
+        return self.__dict__.__setitem__(key, value)
+
+    def __delitem__(self, key):
+        self.__dict__.__delitem__(key)
+
+    def __iter__(self):
+        return self.__dict__.__iter__()
+
+    def __len__(self):
+        return self.__dict__.__len__()
+
+    def __eq__(self, other):
+        if isinstance(other, type(self)):
+            return self.__dict__.__eq__(other.__dict__)
+        return self.__dict__.__eq__(other)
+
+    def __ne__(self, other):
+        if isinstance(other, type(self)):
+            return self.__dict__.__ne__(other.__dict__)
+        return self.__dict__.__ne__(other)
+
+    def __hash__(self):
+        return hash(self.__dict__)
+
+    def __repr__(self):
+        return '{}({})'.format(type(self).__name__, self.__dict__.__repr__())
+
+    def __str__(self):
+        return '{}({})'.format(type(self).__name__, self.__dict__.__str__())
+
+    def clear(self):
+        return self.__dict__.clear()
+
+    def copy(self):
+        ctx = type(self)()
+        ctx.update(self.__dict__)
+        return ctx
+
+    def get(self, key, default=None):
+        return self.__dict__.get(key, default)
+
+    def has_key(self, key):
+        return self.__dict__.has_key(key)  # noqa
+
+    def items(self):
+        return self.__dict__.items()
+
+    def iteritems(self):
+        return self.__dict__.iteritems()
+
+    def iterkeys(self):
+        return self.__dict__.iterkeys()
+
+    def itervalues(self):
+        return self.__dict__.itervalues()
+
+    def keys(self):
+        return self.__dict__.keys()
+
+    def pop(self, key, default=None):
+        return self.__dict__.pop(key, default)
+
+    def popitem(self):
+        return self.__dict__.popitem()
+
+    def setdefault(self, key, default_value=None):
+        return self.__dict__.setdefault(key, default_value)
+
+    def update(self, items):
+        self.__dict__.update(items)
+
+    def values(self):
+        return self.__dict__.values()
+
+    def viewitems(self):
+        return self.__dict__.viewitems()
+
+    def viewkeys(self):
+        return self.__dict__.viewkeys()
+
+    def viewvalues(self):
+        return self.__dict__.viewvalues()
+
+
 class ETag(str):
     """Convenience class to represent a parsed HTTP entity-tag.
 


Test patch:
diff --git a/tests/test_request_context.py b/tests/test_request_context.py
index cc61f86..cd453c5 100644
--- a/tests/test_request_context.py
+++ b/tests/test_request_context.py
@@ -9,14 +9,14 @@ class TestRequestContext(object):
     def test_default_request_context(self):
         env = testing.create_environ()
         req = Request(env)
-        assert type(req.context).__name__ == 'RequestContext'
 
         req.context.hello = 'World'
         assert req.context.hello == 'World'
-        assert 'hello' not in req.context
+        assert req.context['hello'] == 'World'
 
         req.context['note'] = 'Default Request.context_type used to be dict.'
         assert 'note' in req.context
+        assert hasattr(req.context, 'note')
         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 2790e6f..2c90da8 100644
--- a/tests/test_response_context.py
+++ b/tests/test_response_context.py
@@ -7,14 +7,14 @@ class TestResponseContext(object):
 
     def test_default_response_context(self):
         resp = Response()
-        assert type(resp.context).__name__ == 'ResponseContext'
 
         resp.context.hello = 'World!'
         assert resp.context.hello == 'World!'
-        assert 'hello' not in resp.context
+        assert resp.context['hello'] == 'World!'
 
         resp.context['note'] = 'Default Response.context_type used to be dict.'
         assert 'note' in resp.context
+        assert hasattr(resp.context, 'note')
         assert resp.context.get('note') == resp.context['note']
 
     def test_custom_response_context(self):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2626a2c..8bac5cd 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -741,3 +741,154 @@ def test_get_argnames():
     # support, so we just use this regression test to ensure the status quo.
     expected = ['b', 'c'] if compat.PY3 else ['a', 'b', 'c']
     assert misc.get_argnames(functools.partial(foo, 42)) == expected
+
+
+class TestContextType(object):
+
+    class CustomContextType(structures.Context):
+        def __init__(self):
+            pass
+
+    @pytest.mark.parametrize('context_type', [
+        CustomContextType,
+        structures.Context,
+    ])
+    def test_attributes(self, context_type):
+        ctx = context_type()
+
+        ctx.foo = 'bar'
+        ctx.details = None
+        ctx._cache = {}
+
+        assert ctx.foo == 'bar'
+        assert ctx.details is None
+        assert ctx._cache == {}
+
+        with pytest.raises(AttributeError):
+            ctx.cache_strategy
+
+    @pytest.mark.parametrize('context_type', [
+        CustomContextType,
+        structures.Context,
+    ])
+    def test_items_from_attributes(self, context_type):
+        ctx = context_type()
+
+        ctx.foo = 'bar'
+        ctx.details = None
+        ctx._cache = {}
+
+        assert ctx['foo'] == 'bar'
+        assert ctx['details'] is None
+        assert ctx['_cache'] == {}
+
+        with pytest.raises(KeyError):
+            ctx['cache_strategy']
+
+        assert 'foo' in ctx
+        assert '_cache' in ctx
+        assert 'cache_strategy' not in ctx
+
+    @pytest.mark.parametrize('context_type', [
+        CustomContextType,
+        structures.Context,
+    ])
+    def test_attributes_from_items(self, context_type):
+        ctx = context_type()
+
+        ctx['foo'] = 'bar'
+        ctx['details'] = None
+        ctx['_cache'] = {}
+        ctx['cache_strategy'] = 'lru'
+
+        assert ctx['cache_strategy'] == 'lru'
+        del ctx['cache_strategy']
+
+        assert ctx['foo'] == 'bar'
+        assert ctx['details'] is None
+        assert ctx['_cache'] == {}
+
+        with pytest.raises(KeyError):
+            ctx['cache_strategy']
+
+    @pytest.mark.parametrize('context_type,type_name', [
+        (CustomContextType, 'CustomContextType'),
+        (structures.Context, 'Context'),
+    ])
+    def test_dict_interface(self, context_type, type_name):
+        ctx = context_type()
+
+        ctx['foo'] = 'bar'
+        ctx['details'] = None
+        ctx[1] = 'one'
+        ctx[2] = 'two'
+
+        assert ctx == {'foo': 'bar', 'details': None, 1: 'one', 2: 'two'}
+        assert ctx != {'bar': 'foo', 'details': None, 1: 'one', 2: 'two'}
+        assert ctx != {}
+
+        copy = ctx.copy()
+        assert isinstance(copy, context_type)
+        assert copy == ctx
+        assert copy == {'foo': 'bar', 'details': None, 1: 'one', 2: 'two'}
+        copy.pop('foo')
+        assert copy != ctx
+
+        assert set(key for key in ctx) == {'foo', 'details', 1, 2}
+
+        assert ctx.get('foo') == 'bar'
+        assert ctx.get('bar') is None
+        assert ctx.get('bar', frozenset('hello')) == frozenset('hello')
+        false = ctx.get('bar', False)
+        assert isinstance(false, bool)
+        assert not false
+
+        assert len(ctx) == 4
+        assert ctx.pop(3) is None
+        assert ctx.pop(3, 'not found') == 'not found'
+        assert ctx.pop('foo') == 'bar'
+        assert ctx.pop(1) == 'one'
+        assert ctx.pop(2) == 'two'
+        assert len(ctx) == 1
+
+        assert repr(ctx) == type_name + "({'details': None})"
+        assert str(ctx) == type_name + "({'details': None})"
+        assert '{}'.format(ctx) == type_name + "({'details': None})"
+
+        with pytest.raises(TypeError):
+            {ctx: ctx}
+
+        ctx.clear()
+        assert ctx == {}
+        assert len(ctx) == 0
+
+        ctx['key'] = 'value'
+        assert ctx.popitem() == ('key', 'value')
+
+        ctx.setdefault('numbers', []).append(1)
+        ctx.setdefault('numbers', []).append(2)
+        ctx.setdefault('numbers', []).append(3)
+        assert ctx['numbers'] == [1, 2, 3]
+
+    @pytest.mark.parametrize('context_type', [
+        CustomContextType,
+        structures.Context,
+    ])
+    def test_keys_and_values(self, context_type):
+        ctx = context_type()
+        ctx.update((number, number ** 2) for number in range(1, 5))
+
+        assert set(ctx.keys()) == {1, 2, 3, 4}
+        assert set(ctx.values()) == {1, 4, 9, 16}
+        assert set(ctx.items()) == {(1, 1), (2, 4), (3, 9), (4, 16)}
+
+    @pytest.mark.skipif(compat.PY3, reason='python2-specific dict methods')
+    def test_python2_dict_methods(self):
+        ctx = structures.Context()
+        ctx.update((number, number ** 2) for number in range(1, 5))
+
+        assert set(ctx.keys()) == set(ctx.iterkeys()) == set(ctx.viewkeys())
+        assert set(ctx.values()) == set(ctx.itervalues()) == set(ctx.viewvalues())
+        assert set(ctx.items()) == set(ctx.iteritems()) == set(ctx.viewitems())
+
+        assert ctx.has_key(2)  # noqa