summaryrefslogtreecommitdiff
path: root/tests/test_renderengine.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_renderengine.py')
-rw-r--r--tests/test_renderengine.py455
1 files changed, 455 insertions, 0 deletions
diff --git a/tests/test_renderengine.py b/tests/test_renderengine.py
new file mode 100644
index 0000000..6c2831a
--- /dev/null
+++ b/tests/test_renderengine.py
@@ -0,0 +1,455 @@
+# coding: utf-8
+
+"""
+Unit tests of renderengine.py.
+
+"""
+
+import cgi
+import unittest
+
+from pystache.context import Context
+from pystache.parser import ParsingError
+from pystache.renderengine import RenderEngine
+from tests.common import AssertStringMixin
+
+
+class RenderEngineTestCase(unittest.TestCase):
+
+ """Test the RenderEngine class."""
+
+ def test_init(self):
+ """
+ Test that __init__() stores all of the arguments correctly.
+
+ """
+ # In real-life, these arguments would be functions
+ engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
+
+ self.assertEquals(engine.escape, "escape")
+ self.assertEquals(engine.literal, "literal")
+ self.assertEquals(engine.load_partial, "foo")
+
+
+class RenderTests(unittest.TestCase, AssertStringMixin):
+
+ """
+ Tests RenderEngine.render().
+
+ Explicit spec-test-like tests best go in this class since the
+ RenderEngine class contains all parsing logic. This way, the unit tests
+ will be more focused and fail "closer to the code".
+
+ """
+
+ def _engine(self):
+ """
+ Create and return a default RenderEngine for testing.
+
+ """
+ escape = lambda s: unicode(cgi.escape(s))
+ engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ return engine
+
+ def _assert_render(self, expected, template, *context, **kwargs):
+ """
+ Test rendering the given template using the given context.
+
+ """
+ partials = kwargs.get('partials')
+ engine = kwargs.get('engine', self._engine())
+
+ if partials is not None:
+ engine.load_partial = lambda key: unicode(partials[key])
+
+ context = Context(*context)
+
+ actual = engine.render(template, context)
+
+ self.assertString(actual=actual, expected=expected)
+
+ def test_render(self):
+ self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
+
+ def test__load_partial(self):
+ """
+ Test that render() uses the load_template attribute.
+
+ """
+ engine = self._engine()
+ partials = {'partial': u"{{person}}"}
+ engine.load_partial = lambda key: partials[key]
+
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
+
+ def test__literal(self):
+ """
+ Test that render() uses the literal attribute.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper()
+
+ self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine)
+
+ def test_literal__sigil(self):
+ template = "<h1>{{& thing}}</h1>"
+ context = {'thing': 'Bear > Giraffe'}
+
+ expected = u"<h1>Bear > Giraffe</h1>"
+
+ self._assert_render(expected, template, context)
+
+ def test__escape(self):
+ """
+ Test that render() uses the escape attribute.
+
+ """
+ engine = self._engine()
+ engine.escape = lambda s: "**" + s
+
+ self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine)
+
+ def test__escape_does_not_call_literal(self):
+ """
+ Test that render() does not call literal before or after calling escape.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper() # a test version
+ engine.escape = lambda s: "**" + s
+
+ template = 'literal: {{{foo}}} escaped: {{foo}}'
+ context = {'foo': 'bar'}
+
+ self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine)
+
+ def test__escape_preserves_unicode_subclasses(self):
+ """
+ Test that render() preserves unicode subclasses when passing to escape.
+
+ This is useful, for example, if one wants to respect whether a
+ variable value is markupsafe.Markup when escaping.
+
+ """
+ class MyUnicode(unicode):
+ pass
+
+ def escape(s):
+ if type(s) is MyUnicode:
+ return "**" + s
+ else:
+ return s + "**"
+
+ engine = self._engine()
+ engine.escape = escape
+
+ template = '{{foo1}} {{foo2}}'
+ context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'}
+
+ self._assert_render(u'**bar bar**', template, context, engine=engine)
+
+ def test__non_basestring__literal_and_escaped(self):
+ """
+ Test a context value that is not a basestring instance.
+
+ """
+ # We use include upper() to make sure we are actually using
+ # our custom function in the tests
+ to_unicode = lambda s: unicode(s, encoding='ascii').upper()
+ engine = self._engine()
+ engine.escape = to_unicode
+ engine.literal = to_unicode
+
+ self.assertRaises(TypeError, engine.literal, 100)
+
+ template = '{{text}} {{int}} {{{int}}}'
+ context = {'int': 100, 'text': 'foo'}
+
+ self._assert_render(u'FOO 100 100', template, context, engine=engine)
+
+ def test_tag__output_not_interpolated(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{template}}: {{planet}}'
+ context = {'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ def test_tag__output_not_interpolated__section(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{test}}'
+ context = {'test': '{{#hello}}'}
+ self._assert_render(u'{{#hello}}', template, context)
+
+ def test_interpolation__built_in_type__string(self):
+ """
+ Check tag interpolation with a string on the top of the context stack.
+
+ """
+ item = 'abc'
+ # item.upper() == 'ABC'
+ template = '{{#section}}{{upper}}{{/section}}'
+ context = {'section': item, 'upper': 'XYZ'}
+ self._assert_render(u'XYZ', template, context)
+
+ def test_interpolation__built_in_type__integer(self):
+ """
+ Check tag interpolation with an integer on the top of the context stack.
+
+ """
+ item = 10
+ # item.real == 10
+ template = '{{#section}}{{real}}{{/section}}'
+ context = {'section': item, 'real': 1000}
+ self._assert_render(u'1000', template, context)
+
+ def test_interpolation__built_in_type__list(self):
+ """
+ Check tag interpolation with a list on the top of the context stack.
+
+ """
+ item = [[1, 2, 3]]
+ # item[0].pop() == 3
+ template = '{{#section}}{{pop}}{{/section}}'
+ context = {'section': item, 'pop': 7}
+ self._assert_render(u'7', template, context)
+
+ def test_implicit_iterator__literal(self):
+ """
+ Test an implicit iterator in a literal tag.
+
+ """
+ template = """{{#test}}{{{.}}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'<>', template, context)
+
+ def test_implicit_iterator__escaped(self):
+ """
+ Test an implicit iterator in a normal tag.
+
+ """
+ template = """{{#test}}{{.}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'&lt;&gt;', template, context)
+
+ def test_literal__in_section(self):
+ """
+ Check that literals work in sections.
+
+ """
+ template = '{{#test}}1 {{{less_than}}} 2{{/test}}'
+ context = {'test': {'less_than': '<'}}
+
+ self._assert_render(u'1 < 2', template, context)
+
+ def test_literal__in_partial(self):
+ """
+ Check that literals work in partials.
+
+ """
+ template = '{{>partial}}'
+ partials = {'partial': '1 {{{less_than}}} 2'}
+ context = {'less_than': '<'}
+
+ self._assert_render(u'1 < 2', template, context, partials=partials)
+
+ def test_partial(self):
+ partials = {'partial': "{{person}}"}
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials)
+
+ def test_partial__context_values(self):
+ """
+ Test that escape and literal work on context values in partials.
+
+ """
+ engine = self._engine()
+
+ template = '{{>partial}}'
+ partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'}
+ context = {'foo': '<'}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+
+ ## Test cases related specifically to sections.
+
+ def test_section__end_tag_with_no_start_tag(self):
+ """
+ Check what happens if there is an end tag with no start tag.
+
+ """
+ template = '{{/section}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEquals(str(err), "Section end tag mismatch: u'section' != None")
+
+ def test_section__end_tag_mismatch(self):
+ """
+ Check what happens if the end tag doesn't match.
+
+ """
+ template = '{{#section_start}}{{/section_end}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEquals(str(err), "Section end tag mismatch: u'section_end' != u'section_start'")
+
+ def test_section__context_values(self):
+ """
+ Test that escape and literal work on context values in sections.
+
+ """
+ engine = self._engine()
+
+ template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}'
+ context = {'test': {'foo': '<'}}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine)
+
+ def test_section__context_precedence(self):
+ """
+ Check that items higher in the context stack take precedence.
+
+ """
+ template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}'
+ context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}}
+ self._assert_render(u'chicken : beans and rice', template, context)
+
+ def test_section__list_referencing_outer_context(self):
+ """
+ Check that list items can access the parent context.
+
+ For sections whose value is a list, check that items in the list
+ have access to the values inherited from the parent context
+ when rendering.
+
+ """
+ context = {
+ "greeting": "Hi",
+ "list": [{"name": "Al"}, {"name": "Bob"}],
+ }
+
+ template = "{{#list}}{{greeting}} {{name}}, {{/list}}"
+
+ self._assert_render(u"Hi Al, Hi Bob, ", template, context)
+
+ def test_section__output_not_interpolated(self):
+ """
+ Check that rendered section output is not interpolated.
+
+ """
+ template = '{{#section}}{{template}}{{/section}}: {{planet}}'
+ context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ def test_section__nested_truthy(self):
+ """
+ Check that "nested truthy" sections get rendered.
+
+ Test case for issue #24: https://github.com/defunkt/pystache/issues/24
+
+ This test is copied from the spec. We explicitly include it to
+ prevent regressions for those who don't pull down the spec tests.
+
+ """
+ template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |'
+ context = {'bool': True}
+ self._assert_render(u'| A B C D E |', template, context)
+
+ def test_section__nested_with_same_keys(self):
+ """
+ Check a doubly-nested section with the same context key.
+
+ Test case for issue #36: https://github.com/defunkt/pystache/issues/36
+
+ """
+ # Start with an easier, working case.
+ template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}'
+ context = {'x': {'z': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}'
+ context = {'x': {'x': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ def test_section__lambda(self):
+ template = '{{#test}}Mom{{/test}}'
+ context = {'test': (lambda text: 'Hi %s' % text)}
+ self._assert_render(u'Hi Mom', template, context)
+
+ def test_section__iterable(self):
+ """
+ Check that objects supporting iteration (aside from dicts) behave like lists.
+
+ """
+ template = '{{#iterable}}{{.}}{{/iterable}}'
+
+ context = {'iterable': (i for i in range(3))} # type 'generator'
+ self._assert_render(u'012', template, context)
+
+ context = {'iterable': xrange(4)} # type 'xrange'
+ self._assert_render(u'0123', template, context)
+
+ d = {'foo': 0, 'bar': 0}
+ # We don't know what order of keys we'll be given, but from the
+ # Python documentation:
+ # "If items(), keys(), values(), iteritems(), iterkeys(), and
+ # itervalues() are called with no intervening modifications to
+ # the dictionary, the lists will directly correspond."
+ expected = u''.join(d.keys())
+ context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator'
+ self._assert_render(expected, template, context)
+
+ def test_section__lambda__tag_in_output(self):
+ """
+ Check that callable output is treated as a template string (issue #46).
+
+ The spec says--
+
+ When used as the data value for a Section tag, the lambda MUST
+ be treatable as an arity 1 function, and invoked as such (passing
+ a String containing the unprocessed section contents). The
+ returned value MUST be rendered against the current delimiters,
+ then interpolated in place of the section.
+
+ """
+ template = '{{#test}}Hi {{person}}{{/test}}'
+ context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
+ self._assert_render(u'Hi Mom :)', template, context)
+
+ def test_comment__multiline(self):
+ """
+ Check that multiline comments are permitted.
+
+ """
+ self._assert_render(u'foobar', 'foo{{! baz }}bar')
+ self._assert_render(u'foobar', 'foo{{! \nbaz }}bar')
+
+ def test_custom_delimiters__sections(self):
+ """
+ Check that custom delimiters can be used to start a section.
+
+ Test case for issue #20: https://github.com/defunkt/pystache/issues/20
+
+ """
+ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]'
+ context = {'foo': True}
+ self._assert_render(u'bar', template, context)
+
+ def test_custom_delimiters__not_retroactive(self):
+ """
+ Check that changing custom delimiters back is not "retroactive."
+
+ Test case for issue #35: https://github.com/defunkt/pystache/issues/35
+
+ """
+ expected = u' {{foo}} '
+ self._assert_render(expected, '{{=$ $=}} {{foo}} ')
+ self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.