summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2012-05-02 19:17:54 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2012-05-02 19:17:54 -0700
commit384475a56222360331683f2ceb3ecd8d38bf5c7e (patch)
tree2ab26250ee943abd89b110403c7eeee2cf99d172
parent29ac86c0dec2f394f80f076ba3e8c996aa65026c (diff)
parentbd6b5f04fa156b00d2b2831f14f9bdecfbdb8e2a (diff)
downloadpystache-384475a56222360331683f2ceb3ecd8d38bf5c7e.tar.gz
Merge branch 'issue-99-dot-notation' into development:
Addresses issue #99: "Mustache spec v1.1.2 compliance"
-rw-r--r--HISTORY.rst6
-rw-r--r--README.rst4
m---------ext/spec0
-rw-r--r--pystache/context.py94
-rw-r--r--pystache/renderengine.py3
-rw-r--r--pystache/tests/common.py25
-rw-r--r--pystache/tests/test_context.py79
-rw-r--r--pystache/tests/test_renderengine.py77
8 files changed, 250 insertions, 38 deletions
diff --git a/HISTORY.rst b/HISTORY.rst
index 1b3dcd2..a281594 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -4,10 +4,10 @@ History
0.6.0 (TBD)
-----------
+* Added support for dot notation and version 1.1.2 of the spec (issue #99). [rbp]
* Bugfix: falsey values now coerced to strings using str().
-* Bugfix: issue #113: lambda return values for sections no longer pushed
- onto context stack.
-* Bugfix: issue #114: lists of lambdas for sections were not rendered.
+* Bugfix: lambda return values for sections no longer pushed onto context stack (issue #113).
+* Bugfix: lists of lambdas for sections were not rendered (issue #114).
0.5.1 (2012-04-24)
------------------
diff --git a/README.rst b/README.rst
index 71cfe80..e1b5118 100644
--- a/README.rst
+++ b/README.rst
@@ -15,7 +15,7 @@ syntax. For a more complete (and more current) description of Mustache's
behavior, see the official `Mustache spec`_.
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`_
@@ -230,5 +230,5 @@ Authors
.. _TemplateSpec: https://github.com/defunkt/pystache/blob/master/pystache/template_spec.py
.. _test: http://packages.python.org/distribute/setuptools.html#test
.. _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
diff --git a/ext/spec b/ext/spec
-Subproject 48c933b0bb780875acbfd15816297e263c53d6f
+Subproject bf6288ed6bd0ce8ccea6f1dac070b3d779132c3
diff --git a/pystache/context.py b/pystache/context.py
index de22a75..403694c 100644
--- a/pystache/context.py
+++ b/pystache/context.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""
-Exposes a ContextStack class and functions to retrieve names from context.
+Exposes a ContextStack class.
"""
@@ -27,6 +27,8 @@ def _is_callable(obj):
return hasattr(obj, '__call__')
+# TODO: rename item to context (now that we have a separate notion of context stack).
+# TODO: document what a "context" is as opposed to a context stack.
def _get_value(item, key):
"""
Retrieve a key's value from an item.
@@ -60,25 +62,6 @@ def _get_value(item, key):
return _NOT_FOUND
-# TODO: add some unit tests for this.
-def resolve(context, name):
- """
- Resolve the given name against the given context stack.
-
- This function follows the rules outlined in the section of the spec
- regarding tag interpolation.
-
- This function does not coerce the return value to a string.
-
- """
- if name == '.':
- return context.top()
-
- # The spec says that if the name fails resolution, the result should be
- # considered falsey, and should interpolate as the empty string.
- return context.get(name, '')
-
-
class ContextStack(object):
"""
@@ -186,9 +169,65 @@ class ContextStack(object):
return context
- def get(self, key, default=None):
+ # TODO: add some unit tests for this.
+ def get(self, name, default=u''):
"""
- Query the stack for the given key, and return the resulting value.
+ Resolve a dotted name against the current context stack.
+
+ This function follows the rules outlined in the section of the spec
+ regarding tag interpolation.
+
+ Arguments:
+
+ name: a dotted or non-dotted name.
+
+ default: the value to return if name resolution fails at any point.
+ Defaults to the empty string since the Mustache spec says that if
+ name resolution fails at any point, the result should be considered
+ falsey, and should interpolate as the empty string.
+
+ This function does not coerce the return value to a string.
+
+ """
+ if name == '.':
+ # TODO: should we add a test case for an empty context stack?
+ return self.top()
+
+ parts = name.split('.')
+
+ value = self._get_simple(parts[0])
+
+ for part in parts[1:]:
+ # TODO: consider using EAFP here instead.
+ # http://docs.python.org/glossary.html#term-eafp
+ if value is _NOT_FOUND:
+ break
+ # The full context stack is not used to resolve the remaining parts.
+ # From the spec--
+ #
+ # 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)
+
+ if value is _NOT_FOUND:
+ return default
+
+ return value
+
+ # TODO: combine the docstring for this method with the docstring for
+ # the get() method.
+ def _get_simple(self, key):
+ """
+ Query the stack for a non-dotted key, and return the resulting value.
+
+ Arguments:
+
+ key: a non-dotted name.
This method queries items in the stack in order from last-added
objects to first (last in, first out). The value returned is
@@ -253,15 +292,16 @@ class ContextStack(object):
TODO: explain the rationale for this difference in treatment.
"""
- for obj in reversed(self._stack):
- val = _get_value(obj, key)
+ val = _NOT_FOUND
+
+ for item in reversed(self._stack):
+ val = _get_value(item, key)
if val is _NOT_FOUND:
continue
# Otherwise, the key was found.
- return val
- # Otherwise, no item in the stack contained the key.
+ break
- return default
+ return val
def push(self, item):
"""
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index 0ce708b..b05e022 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -7,7 +7,6 @@ Defines a class responsible for rendering logic.
import re
-from pystache.context import resolve
from pystache.parser import Parser
@@ -69,7 +68,7 @@ class RenderEngine(object):
Get a value from the given context as a basestring instance.
"""
- val = resolve(context, tag_name)
+ val = context.get(tag_name)
if callable(val):
# According to the spec:
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 dd9fdae..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
+from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
class SimpleObject(object):
@@ -204,7 +204,7 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop')
-class ContextStackTests(unittest.TestCase, AssertIsMixin):
+class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
Test the ContextStack class.
@@ -320,7 +320,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin):
"""
context = ContextStack()
- self.assertTrue(context.get("foo") is None)
+ self.assertString(context.get("foo"), u'')
def test_get__default(self):
"""
@@ -395,3 +395,76 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin):
# 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 667fa03..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):
@@ -580,3 +580,78 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
expected = u' {{foo}} '
self._assert_render(expected, '{{=$ $=}} {{foo}} ')
self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.
+
+ def test_dot_notation(self):
+ """
+ Test simple dot notation cases.
+
+ 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'}, 'c': {'a': 'A'} }
+ self._assert_render(u'A.B :: (A :: )', template, context)