Commit 4121be33 by Chris Jerdonek

More progress on context module: defined and tested context._get_item().

parent eb736a1c
......@@ -5,6 +5,55 @@ Defines a Context class to represent mustache(5)'s notion of context.
"""
# We use this private global variable as a return value to represent a key
# not being found on lookup. This lets us distinguish the case of a key's
# value being None with the case of a key not being found -- without having
# to rely on exceptions (e.g. KeyError) for flow control.
_NOT_FOUND = object()
# TODO: share code with template.check_callable().
def _is_callable(obj):
return hasattr(obj, '__call__')
def _get_item(obj, key):
"""
Look up the given key in the given object, and return the value.
The obj argument should satisfy the same conditions as those described
in Context.__init__'s() docstring. The behavior of this method is
undefined if obj is None.
The rules for querying are the same as the rules described in
Context.get()'s docstring for a single item.
Returns _NOT_FOUND if the key is not found.
"""
if hasattr(obj, '__getitem__'):
# We do a membership test to avoid using exceptions for flow
# control. In addition, we call __contains__() explicitly as
# opposed to using the membership operator "in" to avoid
# triggering the following Python fallback behavior:
#
# For objects that don’t define __contains__(), the membership test
# first tries iteration via __iter__(), then the old sequence
# iteration protocol via __getitem__()....
#
# (from http://docs.python.org/reference/datamodel.html#object.__contains__ )
if obj.__contains__(key):
return obj[key]
elif hasattr(obj, key):
attr = getattr(obj, key)
if _is_callable(attr):
return attr()
return attr
return _NOT_FOUND
class Context(object):
......@@ -33,6 +82,19 @@ class Context(object):
(2) If they implement __getitem__, a KeyError should be raised
if __getitem__ is called on a missing key.
For efficiency, objects should implement __contains__() for more
efficient membership testing. From the Python documentation--
For objects that don’t define __contains__(), the membership test
first tries iteration via __iter__(), then the old sequence
iteration protocol via __getitem__()....
(from http://docs.python.org/reference/datamodel.html#object.__contains__ )
Failing to implement __contains__() will cause undefined behavior.
on any key for which __getitem__() raises an exception [TODO:
also need to take __iter__() into account]....
"""
self.stack = list(obj)
......@@ -51,10 +113,12 @@ class Context(object):
"""
for obj in reversed(self.stack):
try:
return obj[key]
except KeyError:
pass
val = _get_item(obj, key)
if val is _NOT_FOUND:
continue
# Otherwise, the key was found.
return val
# Otherwise, no item in the stack contained the key.
return default
......
......@@ -7,10 +7,131 @@ Unit tests of context.py.
import unittest
from pystache.context import _NOT_FOUND
from pystache.context import _get_item
from pystache.context import Context
class ContextTestCase(unittest.TestCase):
class TestCase(unittest.TestCase):
"""A TestCase class with support for assertIs()."""
# unittest.assertIs() is not available until Python 2.7:
# http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
def assertIs(self, first, second):
self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
class SimpleObject(object):
"""A sample class that does not define __getitem__()."""
def __init__(self):
self.foo = "bar"
def foo_callable(self):
return "called..."
class MappingObject(object):
"""A sample class that implements __getitem__() and __contains__()."""
def __init__(self):
self._dict = {'foo': 'bar'}
self.fuzz = 'buzz'
def __contains__(self, key):
return key in self._dict
def __getitem__(self, key):
return self._dict[key]
class GetItemTestCase(TestCase):
"""Test context._get_item()."""
def assertNotFound(self, obj, key):
self.assertIs(_get_item(obj, key), _NOT_FOUND)
### Case: obj is a dictionary.
def test_dictionary__key_present(self):
"""
Test getting a key from a dictionary.
"""
obj = {"foo": "bar"}
self.assertEquals(_get_item(obj, "foo"), "bar")
def test_dictionary__key_missing(self):
"""
Test getting a missing key from a dictionary.
"""
obj = {}
self.assertNotFound(obj, "missing")
### Case: obj does not implement __getitem__().
def test_object__attribute_present(self):
"""
Test getting an attribute from an object.
"""
obj = SimpleObject()
self.assertEquals(_get_item(obj, "foo"), "bar")
def test_object__attribute_missing(self):
"""
Test getting a missing attribute from an object.
"""
obj = SimpleObject()
self.assertNotFound(obj, "missing")
### Case: obj implements __getitem__() (i.e. a "mapping object").
def test_mapping__key_present(self):
"""
Test getting a key from a mapping object.
"""
obj = MappingObject()
self.assertEquals(_get_item(obj, "foo"), "bar")
def test_mapping__key_missing(self):
"""
Test getting a missing key from a mapping object.
"""
obj = MappingObject()
self.assertNotFound(obj, "missing")
def test_mapping__get_attribute(self):
"""
Test getting an attribute from a mapping object.
"""
obj = MappingObject()
self.assertEquals(obj.fuzz, "buzz")
self.assertNotFound(obj, "fuzz")
def test_mapping_object__not_implementing_contains(self):
"""
Test querying a mapping object that doesn't define __contains__().
"""
class Sample(object):
def __getitem__(self, key):
return "bar"
obj = Sample()
self.assertRaises(AttributeError, _get_item, obj, "foo")
class ContextTestCase(TestCase):
"""
Test the Context class.
......@@ -24,7 +145,7 @@ class ContextTestCase(unittest.TestCase):
"""
context = Context()
def test_init__no_elements(self):
def test_init__many_elements(self):
"""
Check that passing more than two items to __init__() raises no exception.
......@@ -39,6 +160,14 @@ class ContextTestCase(unittest.TestCase):
context = Context()
self.assertTrue(context.get("foo") is None)
def test_get__dictionary_methods_not_queried(self):
"""
Test getting a missing key.
"""
context = Context()
#self.assertEquals(context.get("keys"), 2)
def test_get__default(self):
"""
Test that get() respects the default value .
......@@ -71,3 +200,19 @@ class ContextTestCase(unittest.TestCase):
context = Context({"fuzz": "buzz"}, {"foo": "bar"})
self.assertEquals(context.get("fuzz"), "buzz")
def test_get__object_attribute(self):
"""
Test that object attributes are queried.
"""
context = Context(SimpleObject())
self.assertEquals(context.get("foo"), "bar")
def test_get__object_callable(self):
"""
Test that object callables are queried.
"""
context = Context(SimpleObject())
#self.assertEquals(context.get("foo_callable"), "called...")
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment