diff options
author | Chris Jerdonek <chris.jerdonek@gmail.com> | 2012-05-02 18:56:10 -0700 |
---|---|---|
committer | Chris Jerdonek <chris.jerdonek@gmail.com> | 2012-05-02 19:07:14 -0700 |
commit | 9dc92ba1d3d941742f85357386c86766cb698860 (patch) | |
tree | 563c12545f90cb50c37213d5aa60ba06aee2d3f3 /pystache | |
parent | 77a6a23284632a127573c92dcc45a904c3fec8ac (diff) | |
parent | 8180ef7a331677f3a8975c14fc73c099a174a3e9 (diff) | |
download | pystache-9dc92ba1d3d941742f85357386c86766cb698860.tar.gz |
Merge remote-tracking branch 'rbp/development' into issue-99-dot-notation:
This is pull request #100 for issue #99: https://github.com/defunkt/pystache/issues/99
Adds test cases and incorporates some clean-ups.
Conflicts:
pystache/tests/test_context.py
pystache/tests/test_renderengine.py
Diffstat (limited to 'pystache')
-rw-r--r-- | pystache/context.py | 8 | ||||
-rw-r--r-- | pystache/tests/common.py | 25 | ||||
-rw-r--r-- | pystache/tests/test_context.py | 75 | ||||
-rw-r--r-- | pystache/tests/test_renderengine.py | 76 |
4 files changed, 173 insertions, 11 deletions
diff --git a/pystache/context.py b/pystache/context.py index cf36c7d..403694c 100644 --- a/pystache/context.py +++ b/pystache/context.py @@ -205,9 +205,11 @@ class ContextStack(object): # The full context stack is not used to resolve the remaining parts. # From the spec-- # - # If any name parts were retained in step 1, each should be resolved - # against a context stack containing only the result from the former - # resolution. + # 5) If any name parts were retained in step 1, each should be + # resolved against a context stack containing only the result + # from the former resolution. If any part fails resolution, the + # result should be considered falsey, and should interpolate as + # the empty string. # # TODO: make sure we have a test case for the above point. value = _get_value(value, part) diff --git a/pystache/tests/common.py b/pystache/tests/common.py index a99e709..4c8f46c 100644 --- a/pystache/tests/common.py +++ b/pystache/tests/common.py @@ -191,3 +191,28 @@ class SetupDefaults(object): defaults.FILE_ENCODING = self.original_file_encoding defaults.STRING_ENCODING = self.original_string_encoding + +class Attachable(object): + """ + A class that attaches all constructor named parameters as attributes. + + For example-- + + >>> obj = Attachable(foo=42, size="of the universe") + >>> repr(obj) + "Attachable(foo=42, size='of the universe')" + >>> obj.foo + 42 + >>> obj.size + 'of the universe' + + """ + def __init__(self, **kwargs): + self.__args__ = kwargs + for arg, value in kwargs.iteritems(): + setattr(self, arg, value) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, + ", ".join("%s=%s" % (k, repr(v)) + for k, v in self.__args__.iteritems())) diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py index 538c74e..0c5097b 100644 --- a/pystache/tests/test_context.py +++ b/pystache/tests/test_context.py @@ -11,7 +11,7 @@ import unittest from pystache.context import _NOT_FOUND from pystache.context import _get_value from pystache.context import ContextStack -from pystache.tests.common import AssertIsMixin, AssertStringMixin +from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable class SimpleObject(object): @@ -395,3 +395,76 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): # Confirm the original is unchanged. self.assertEqual(original.get(key), "buzz") + def test_dot_notation__dict(self): + name = "foo.bar" + stack = ContextStack({"foo": {"bar": "baz"}}) + self.assertEqual(stack.get(name), "baz") + + # Works all the way down + name = "a.b.c.d.e.f.g" + stack = ContextStack({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}}) + self.assertEqual(stack.get(name), "w00t!") + + def test_dot_notation__user_object(self): + name = "foo.bar" + stack = ContextStack({"foo": Attachable(bar="baz")}) + self.assertEquals(stack.get(name), "baz") + + # Works on multiple levels, too + name = "a.b.c.d.e.f.g" + A = Attachable + stack = ContextStack({"a": A(b=A(c=A(d=A(e=A(f=A(g="w00t!"))))))}) + self.assertEquals(stack.get(name), "w00t!") + + def test_dot_notation__mixed_dict_and_obj(self): + name = "foo.bar.baz.bak" + stack = ContextStack({"foo": Attachable(bar={"baz": Attachable(bak=42)})}) + self.assertEquals(stack.get(name), 42) + + def test_dot_notation__missing_attr_or_key(self): + name = "foo.bar.baz.bak" + stack = ContextStack({"foo": {"bar": {}}}) + self.assertString(stack.get(name), u'') + + stack = ContextStack({"foo": Attachable(bar=Attachable())}) + self.assertString(stack.get(name), u'') + + def test_dot_notation__missing_part_terminates_search(self): + """ + Test that dotted name resolution terminates on a later part not found. + + Check that if a later dotted name part is not found in the result from + the former resolution, then name resolution terminates rather than + starting the search over with the next element of the context stack. + From the spec (interpolation section)-- + + 5) If any name parts were retained in step 1, each should be resolved + against a context stack containing only the result from the former + resolution. If any part fails resolution, the result should be considered + falsey, and should interpolate as the empty string. + + This test case is equivalent to the test case in the following pull + request: + + https://github.com/mustache/spec/pull/48 + + """ + stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'}) + self.assertEqual(stack.get('a'), 'A') + self.assertString(stack.get('a.b'), u'') + stack.pop() + self.assertEqual(stack.get('a.b'), 'A.B') + + def test_dot_notation__autocall(self): + name = "foo.bar.baz" + + # When any element in the path is callable, it should be automatically invoked + stack = ContextStack({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))}) + self.assertEquals(stack.get(name), "Called!") + + class Foo(object): + def bar(self): + return Attachable(baz='Baz') + + stack = ContextStack({"foo": Foo()}) + self.assertEquals(stack.get(name), "Baz") diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py index adbae7b..d7f6bf7 100644 --- a/pystache/tests/test_renderengine.py +++ b/pystache/tests/test_renderengine.py @@ -11,7 +11,7 @@ from pystache.context import ContextStack from pystache import defaults from pystache.parser import ParsingError from pystache.renderengine import RenderEngine -from pystache.tests.common import AssertStringMixin +from pystache.tests.common import AssertStringMixin, Attachable def mock_literal(s): @@ -581,15 +581,77 @@ class RenderTests(unittest.TestCase, AssertStringMixin): self._assert_render(expected, '{{=$ $=}} {{foo}} ') self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. - def test_dot_notation__forward_progress(self): + def test_dot_notation(self): """ - Test that dotted name resolution makes "forward-only" progress. + Test simple dot notation cases. - This is equivalent to the test case in the following pull request: + Check that we can use dot notation when the variable is a dict, + user-defined object, or combination of both. + + """ + template = 'Hello, {{person.name}}. I see you are {{person.details.age}}.' + person = Attachable(name='Biggles', details={'age': 42}) + context = {'person': person} + self._assert_render(u'Hello, Biggles. I see you are 42.', template, context) + + def test_dot_notation__missing_attributes_or_keys(self): + """ + Test dot notation with missing keys or attributes. + + Check that if a key or attribute in a dotted name does not exist, then + the tag renders as the empty string. + + """ + template = """I cannot see {{person.name}}'s age: {{person.age}}. + Nor {{other_person.name}}'s: .""" + expected = u"""I cannot see Biggles's age: . + Nor Mr. Bradshaw's: .""" + context = {'person': {'name': 'Biggles'}, + 'other_person': Attachable(name='Mr. Bradshaw')} + self._assert_render(expected, template, context) + + def test_dot_notation__multiple_levels(self): + """ + Test dot notation with multiple levels. + + """ + template = """Hello, Mr. {{person.name.lastname}}. + I see you're back from {{person.travels.last.country.city}}. + I'm missing some of your details: {{person.details.private.editor}}.""" + expected = u"""Hello, Mr. Pither. + I see you're back from Cornwall. + I'm missing some of your details: .""" + context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'}, + 'travels': {'last': {'country': {'city': 'Cornwall'}}}, + 'details': {'public': 'likes cycling'}}} + self._assert_render(expected, template, context) + + # It should also work with user-defined objects + context = {'person': Attachable(name={'firstname': 'unknown', 'lastname': 'Pither'}, + travels=Attachable(last=Attachable(country=Attachable(city='Cornwall'))), + details=Attachable())} + self._assert_render(expected, template, context) + + def test_dot_notation__missing_part_terminates_search(self): + """ + Test that dotted name resolution terminates on a later part not found. + + Check that if a later dotted name part is not found in the result from + the former resolution, then name resolution terminates rather than + starting the search over with the next element of the context stack. + From the spec (interpolation section)-- + + 5) If any name parts were retained in step 1, each should be resolved + against a context stack containing only the result from the former + resolution. If any part fails resolution, the result should be considered + falsey, and should interpolate as the empty string. + + This test case is equivalent to the test case in the following pull + request: https://github.com/mustache/spec/pull/48 """ - template = '{{a.b}} :: {{#c}}{{a}} :: {{a.b}}{{/c}}' - context = {'a': {'b': 'a.b found'}, 'c': {'a': 'a.b not found'} } - self._assert_render(u'a.b found :: a.b not found :: ', template, context) + template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})' + context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} } + self._assert_render(u'A.B :: (A :: )', template, context) |