diff --git a/pystache/context.py b/pystache/context.py index 67159160..7ceeffea 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -7,13 +7,21 @@ stack elements: hashes and objects. For the purposes of interpreting the spec, we define these categories mutually exclusively as follows: - (1) Hash: an item whose type is a subclass of dict. + (1) Hash: an item whose type is a subclass of collections.Mapping. + + .. note:: + + collections.Mapping is used instead of a dict in order to allow other + mapping types to be valid contexts. See + `Issue 144 ` (2) Object: an item that is neither a hash nor an instance of a built-in type. """ +import collections + from pystache.common import PystacheError @@ -43,8 +51,11 @@ def _get_value(context, key): The ContextStack.get() docstring documents this function's intended behavior. """ - if isinstance(context, dict): + if isinstance(context, collections.Mapping): # Then we consider the argument a "hash" for the purposes of the spec. + # Using collections.Mapping allows other mapping types to be registered + # as valid "hash" contexts. See + # `Issue 144 ` # # We do a membership test to avoid using exceptions for flow control # (e.g. catching KeyError). diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py index 238e4b00..a7de6333 100644 --- a/pystache/tests/test_context.py +++ b/pystache/tests/test_context.py @@ -7,6 +7,7 @@ from datetime import datetime import unittest +import collections from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable @@ -102,6 +103,51 @@ class DictSubclass(dict): pass self.assertEqual(_get_value(item, "foo"), "bar") + def test_dictionary__mapping_implementation(self): + """ + Test that registered implementations of collections.Mapping are treated as dictionaries. + + See https://github.com/defunkt/pystache/pull/144 + + """ + class MappingSubclass(collections.Mapping): + + def __init__(self, *args, **kwargs): + self._mapping = dict(*args, **kwargs) + super(MappingSubclass, self).__init__() + + def __getitem__(self, key): + return self._mapping[key] + + def __iter__(self): + return iter(self._mapping) + + def __len__(self): + return len(self._mapping) + + class RegisteredMapping(object): + + def __init__(self, *args, **kwargs): + self._mapping = dict(*args, **kwargs) + super(RegisteredMapping, self).__init__() + + def __getitem__(self, key): + return self._mapping[key] + + def __iter__(self): + return iter(self._mapping) + + def __len__(self): + return len(self._mapping) + + collections.Mapping.register(RegisteredMapping) + + item = MappingSubclass(foo="bar") + self.assertEqual(_get_value(item, "foo"), "bar") + + item = RegisteredMapping(foo="bar") + self.assertEqual(_get_value(item, "foo"), "bar") + ### Case: the item is an object. def test_object__attribute_present(self):