Commit 9dc92ba1 by Chris Jerdonek

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
parents 77a6a232 8180ef7a
...@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's ...@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's
behavior, see the official `Mustache spec`_. behavior, see the official `Mustache spec`_.
Pystache is `semantically versioned`_ and can be found on PyPI_. This Pystache is `semantically versioned`_ and can be found on PyPI_. This
version of Pystache passes all tests in `version 1.0.3`_ of the spec. version of Pystache passes all tests in `version 1.1.2`_ of the spec.
Logo: `David Phillips`_ Logo: `David Phillips`_
...@@ -230,5 +230,5 @@ Authors ...@@ -230,5 +230,5 @@ Authors
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py .. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _test: http://packages.python.org/distribute/setuptools.html#test .. _test: http://packages.python.org/distribute/setuptools.html#test
.. _tox: http://pypi.python.org/pypi/tox .. _tox: http://pypi.python.org/pypi/tox
.. _version 1.0.3: https://github.com/mustache/spec/tree/48c933b0bb780875acbfd15816297e263c53d6f7 .. _version 1.1.2: https://github.com/mustache/spec/tree/v1.1.2
.. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9 .. _version 2.0.9: http://pypi.python.org/pypi/simplejson/2.0.9
Subproject commit 48c933b0bb780875acbfd15816297e263c53d6f7 Subproject commit bf6288ed6bd0ce8ccea6f1dac070b3d779132c3b
...@@ -205,9 +205,11 @@ class ContextStack(object): ...@@ -205,9 +205,11 @@ class ContextStack(object):
# The full context stack is not used to resolve the remaining parts. # The full context stack is not used to resolve the remaining parts.
# From the spec-- # From the spec--
# #
# If any name parts were retained in step 1, each should be resolved # 5) If any name parts were retained in step 1, each should be
# against a context stack containing only the result from the former # resolved against a context stack containing only the result
# resolution. # 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. # TODO: make sure we have a test case for the above point.
value = _get_value(value, part) value = _get_value(value, part)
......
...@@ -191,3 +191,28 @@ class SetupDefaults(object): ...@@ -191,3 +191,28 @@ class SetupDefaults(object):
defaults.FILE_ENCODING = self.original_file_encoding defaults.FILE_ENCODING = self.original_file_encoding
defaults.STRING_ENCODING = self.original_string_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()))
...@@ -11,7 +11,7 @@ import unittest ...@@ -11,7 +11,7 @@ import unittest
from pystache.context import _NOT_FOUND from pystache.context import _NOT_FOUND
from pystache.context import _get_value from pystache.context import _get_value
from pystache.context import ContextStack from pystache.context import ContextStack
from pystache.tests.common import AssertIsMixin, AssertStringMixin from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
class SimpleObject(object): class SimpleObject(object):
...@@ -395,3 +395,76 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -395,3 +395,76 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
# Confirm the original is unchanged. # Confirm the original is unchanged.
self.assertEqual(original.get(key), "buzz") 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")
...@@ -11,7 +11,7 @@ from pystache.context import ContextStack ...@@ -11,7 +11,7 @@ from pystache.context import ContextStack
from pystache import defaults from pystache import defaults
from pystache.parser import ParsingError from pystache.parser import ParsingError
from pystache.renderengine import RenderEngine from pystache.renderengine import RenderEngine
from pystache.tests.common import AssertStringMixin from pystache.tests.common import AssertStringMixin, Attachable
def mock_literal(s): def mock_literal(s):
...@@ -581,15 +581,77 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -581,15 +581,77 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(expected, '{{=$ $=}} {{foo}} ') self._assert_render(expected, '{{=$ $=}} {{foo}} ')
self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '. 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 https://github.com/mustache/spec/pull/48
""" """
template = '{{a.b}} :: {{#c}}{{a}} :: {{a.b}}{{/c}}' template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context = {'a': {'b': 'a.b found'}, 'c': {'a': 'a.b not found'} } context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} }
self._assert_render(u'a.b found :: a.b not found :: ', template, context) self._assert_render(u'A.B :: (A :: )', template, context)
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