Commit ad2686cf by Chris Jerdonek

Merge branch 'issue-110-not-found-tags' into 'development':

Finished addressing issue #110: "Missing tag values always treated as empty"
parents 170c65df 93a2a84e
...@@ -4,6 +4,8 @@ History ...@@ -4,6 +4,8 @@ History
0.5.3 (TBD) 0.5.3 (TBD)
----------- -----------
* Added option of raising errors on missing tags/partials:
``Renderer(missing_tags='strict')`` (issue #110).
* Bugfix: exceptions raised from a property are no longer swallowed when * Bugfix: exceptions raised from a property are no longer swallowed when
getting a key from a context stack (issue #110). getting a key from a context stack (issue #110).
......
...@@ -26,6 +26,14 @@ def read(path): ...@@ -26,6 +26,14 @@ def read(path):
f.close() f.close()
class MissingTags(object):
"""Contains the valid values for Renderer.missing_tags."""
ignore = 'ignore'
strict = 'strict'
class PystacheError(Exception): class PystacheError(Exception):
"""Base class for Pystache exceptions.""" """Base class for Pystache exceptions."""
pass pass
......
...@@ -14,6 +14,9 @@ spec, we define these categories mutually exclusively as follows: ...@@ -14,6 +14,9 @@ spec, we define these categories mutually exclusively as follows:
""" """
from pystache.common import PystacheError
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3. # This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__ _BUILTIN_MODULE = type(0).__module__
...@@ -73,6 +76,21 @@ def _get_value(context, key): ...@@ -73,6 +76,21 @@ def _get_value(context, key):
return _NOT_FOUND return _NOT_FOUND
class KeyNotFoundError(PystacheError):
"""
An exception raised when a key is not found in a context stack.
"""
def __init__(self, key, details):
self.key = key
self.details = details
def __str__(self):
return "Key %s not found: %s" % (repr(self.key), self.details)
class ContextStack(object): class ContextStack(object):
""" """
...@@ -182,7 +200,7 @@ class ContextStack(object): ...@@ -182,7 +200,7 @@ class ContextStack(object):
# TODO: add more unit tests for this. # TODO: add more unit tests for this.
# TODO: update the docstring for dotted names. # TODO: update the docstring for dotted names.
def get(self, name, default=u''): def get(self, name):
""" """
Resolve a dotted name against the current context stack. Resolve a dotted name against the current context stack.
...@@ -252,18 +270,19 @@ class ContextStack(object): ...@@ -252,18 +270,19 @@ class ContextStack(object):
""" """
if name == '.': if name == '.':
# TODO: should we add a test case for an empty context stack? try:
return self.top() return self.top()
except IndexError:
raise KeyNotFoundError(".", "empty context stack")
parts = name.split('.') parts = name.split('.')
result = self._get_simple(parts[0]) try:
result = self._get_simple(parts[0])
except KeyNotFoundError:
raise KeyNotFoundError(name, "first part")
for part in parts[1:]: for part in parts[1:]:
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if result is _NOT_FOUND:
break
# 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--
# #
...@@ -275,9 +294,10 @@ class ContextStack(object): ...@@ -275,9 +294,10 @@ class ContextStack(object):
# #
# TODO: make sure we have a test case for the above point. # TODO: make sure we have a test case for the above point.
result = _get_value(result, part) result = _get_value(result, part)
# TODO: consider using EAFP here instead.
if result is _NOT_FOUND: # http://docs.python.org/glossary.html#term-eafp
return default if result is _NOT_FOUND:
raise KeyNotFoundError(name, "missing %s" % repr(part))
return result return result
...@@ -286,16 +306,12 @@ class ContextStack(object): ...@@ -286,16 +306,12 @@ class ContextStack(object):
Query the stack for a non-dotted name. Query the stack for a non-dotted name.
""" """
result = _NOT_FOUND
for item in reversed(self._stack): for item in reversed(self._stack):
result = _get_value(item, name) result = _get_value(item, name)
if result is _NOT_FOUND: if result is not _NOT_FOUND:
continue return result
# Otherwise, the key was found.
break
return result raise KeyNotFoundError(name, "part missing")
def push(self, item): def push(self, item):
""" """
......
...@@ -17,6 +17,8 @@ except ImportError: ...@@ -17,6 +17,8 @@ except ImportError:
import os import os
import sys import sys
from pystache.common import MissingTags
# How to handle encoding errors when decoding strings from str to unicode. # How to handle encoding errors when decoding strings from str to unicode.
# #
...@@ -36,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding() ...@@ -36,6 +38,9 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files. # strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding() FILE_ENCODING = sys.getdefaultencoding()
# How to handle missing tags when rendering a template.
MISSING_TAGS = MissingTags.ignore
# The starting list of directories in which to search for templates when # The starting list of directories in which to search for templates when
# loading a template by file name. # loading a template by file name.
SEARCH_DIRS = [os.curdir] # i.e. ['.'] SEARCH_DIRS = [os.curdir] # i.e. ['.']
......
...@@ -9,7 +9,6 @@ This module is only meant for internal use by the renderengine module. ...@@ -9,7 +9,6 @@ This module is only meant for internal use by the renderengine module.
import re import re
from pystache.common import TemplateNotFoundError
from pystache.parsed import ParsedTemplate from pystache.parsed import ParsedTemplate
...@@ -216,15 +215,9 @@ class Parser(object): ...@@ -216,15 +215,9 @@ class Parser(object):
elif tag_type == '>': elif tag_type == '>':
try: template = engine.resolve_partial(tag_key)
# TODO: make engine.load() and test it separately.
template = engine.load_partial(tag_key)
except TemplateNotFoundError:
template = u''
# Indent before rendering. # Indent before rendering.
template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template) template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
func = engine._make_get_partial(template) func = engine._make_get_partial(template)
else: else:
......
...@@ -10,6 +10,14 @@ import re ...@@ -10,6 +10,14 @@ import re
from pystache.parser import Parser from pystache.parser import Parser
def context_get(stack, name):
"""
Find and return a name from a ContextStack instance.
"""
return stack.get(name)
class RenderEngine(object): class RenderEngine(object):
""" """
...@@ -29,15 +37,11 @@ class RenderEngine(object): ...@@ -29,15 +37,11 @@ class RenderEngine(object):
""" """
def __init__(self, load_partial=None, literal=None, escape=None): def __init__(self, literal=None, escape=None, resolve_context=None,
resolve_partial=None):
""" """
Arguments: Arguments:
load_partial: the function to call when loading a partial. The
function should accept a string template name and return a
template string of type unicode (not a subclass). If the
template is not found, it should raise a TemplateNotFoundError.
literal: the function used to convert unescaped variable tag literal: the function used to convert unescaped variable tag
values to unicode, e.g. the value corresponding to a tag values to unicode, e.g. the value corresponding to a tag
"{{{name}}}". The function should accept a string of type "{{{name}}}". The function should accept a string of type
...@@ -59,18 +63,27 @@ class RenderEngine(object): ...@@ -59,18 +63,27 @@ class RenderEngine(object):
incoming strings of type markupsafe.Markup differently incoming strings of type markupsafe.Markup differently
from plain unicode strings. from plain unicode strings.
resolve_context: the function to call to resolve a name against
a context stack. The function should accept two positional
arguments: a ContextStack instance and a name to resolve.
resolve_partial: the function to call when loading a partial.
The function should accept a template name string and return a
template string of type unicode (not a subclass).
""" """
self.escape = escape self.escape = escape
self.literal = literal self.literal = literal
self.load_partial = load_partial self.resolve_context = resolve_context
self.resolve_partial = resolve_partial
# TODO: rename context to stack throughout this module. # TODO: Rename context to stack throughout this module.
def _get_string_value(self, context, tag_name): def _get_string_value(self, context, tag_name):
""" """
Get a value from the given context as a basestring instance. Get a value from the given context as a basestring instance.
""" """
val = context.get(tag_name) val = self.resolve_context(context, tag_name)
if callable(val): if callable(val):
# According to the spec: # According to the spec:
...@@ -138,7 +151,7 @@ class RenderEngine(object): ...@@ -138,7 +151,7 @@ class RenderEngine(object):
""" """
# TODO: is there a bug because we are not using the same # TODO: is there a bug because we are not using the same
# logic as in _get_string_value()? # logic as in _get_string_value()?
data = context.get(name) data = self.resolve_context(context, name)
# Per the spec, lambdas in inverted sections are considered truthy. # Per the spec, lambdas in inverted sections are considered truthy.
if data: if data:
return u'' return u''
...@@ -157,7 +170,7 @@ class RenderEngine(object): ...@@ -157,7 +170,7 @@ class RenderEngine(object):
""" """
template = template_ template = template_
parsed_template = parsed_template_ parsed_template = parsed_template_
data = context.get(name) data = self.resolve_context(context, name)
# From the spec: # From the spec:
# #
......
...@@ -8,10 +8,10 @@ This module provides a Renderer class to render templates. ...@@ -8,10 +8,10 @@ This module provides a Renderer class to render templates.
import sys import sys
from pystache import defaults from pystache import defaults
from pystache.common import TemplateNotFoundError from pystache.common import TemplateNotFoundError, MissingTags
from pystache.context import ContextStack from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader from pystache.loader import Loader
from pystache.renderengine import RenderEngine from pystache.renderengine import context_get, RenderEngine
from pystache.specloader import SpecLoader from pystache.specloader import SpecLoader
from pystache.template_spec import TemplateSpec from pystache.template_spec import TemplateSpec
...@@ -27,6 +27,7 @@ else: ...@@ -27,6 +27,7 @@ else:
_STRING_TYPES = (unicode, type(u"a".encode('utf-8'))) _STRING_TYPES = (unicode, type(u"a".encode('utf-8')))
class Renderer(object): class Renderer(object):
""" """
...@@ -49,7 +50,7 @@ class Renderer(object): ...@@ -49,7 +50,7 @@ class Renderer(object):
def __init__(self, file_encoding=None, string_encoding=None, def __init__(self, file_encoding=None, string_encoding=None,
decode_errors=None, search_dirs=None, file_extension=None, decode_errors=None, search_dirs=None, file_extension=None,
escape=None, partials=None): escape=None, partials=None, missing_tags=None):
""" """
Construct an instance. Construct an instance.
...@@ -104,6 +105,11 @@ class Renderer(object): ...@@ -104,6 +105,11 @@ class Renderer(object):
argument to the built-in function unicode(). Defaults to the argument to the built-in function unicode(). Defaults to the
package default. package default.
missing_tags: a string specifying how to handle missing tags.
If 'strict', an error is raised on a missing tag. If 'ignore',
the value of the tag is the empty string. Defaults to the
package default.
""" """
if decode_errors is None: if decode_errors is None:
decode_errors = defaults.DECODE_ERRORS decode_errors = defaults.DECODE_ERRORS
...@@ -117,6 +123,9 @@ class Renderer(object): ...@@ -117,6 +123,9 @@ class Renderer(object):
if file_extension is None: if file_extension is None:
file_extension = defaults.TEMPLATE_EXTENSION file_extension = defaults.TEMPLATE_EXTENSION
if missing_tags is None:
missing_tags = defaults.MISSING_TAGS
if search_dirs is None: if search_dirs is None:
search_dirs = defaults.SEARCH_DIRS search_dirs = defaults.SEARCH_DIRS
...@@ -131,6 +140,7 @@ class Renderer(object): ...@@ -131,6 +140,7 @@ class Renderer(object):
self.escape = escape self.escape = escape
self.file_encoding = file_encoding self.file_encoding = file_encoding
self.file_extension = file_extension self.file_extension = file_extension
self.missing_tags = missing_tags
self.partials = partials self.partials = partials
self.search_dirs = search_dirs self.search_dirs = search_dirs
self.string_encoding = string_encoding self.string_encoding = string_encoding
...@@ -224,21 +234,21 @@ class Renderer(object): ...@@ -224,21 +234,21 @@ class Renderer(object):
def _make_load_partial(self): def _make_load_partial(self):
""" """
Return the load_partial function to pass to RenderEngine.__init__(). Return a function that loads a partial by name.
""" """
if self.partials is None: if self.partials is None:
load_template = self._make_load_template() return self._make_load_template()
return load_template
# Otherwise, create a load_partial function from the custom partial # Otherwise, create a function from the custom partial loader.
# loader that satisfies RenderEngine requirements (and that provides
# a nicer exception, etc).
partials = self.partials partials = self.partials
def load_partial(name): def load_partial(name):
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
# This would mean requiring that the custom partial loader
# raise a KeyError on name not found.
template = partials.get(name) template = partials.get(name)
if template is None: if template is None:
raise TemplateNotFoundError("Name %s not found in partials: %s" % raise TemplateNotFoundError("Name %s not found in partials: %s" %
(repr(name), type(partials))) (repr(name), type(partials)))
...@@ -248,16 +258,61 @@ class Renderer(object): ...@@ -248,16 +258,61 @@ class Renderer(object):
return load_partial return load_partial
def _is_missing_tags_strict(self):
"""
Return whether missing_tags is set to strict.
"""
return self.missing_tags == MissingTags.strict
def _make_resolve_partial(self):
"""
Return the resolve_partial function to pass to RenderEngine.__init__().
"""
load_partial = self._make_load_partial()
if self._is_missing_tags_strict():
return load_partial
# Otherwise, ignore missing tags.
def resolve_partial(name):
try:
return load_partial(name)
except TemplateNotFoundError:
return u''
return resolve_partial
def _make_resolve_context(self):
"""
Return the resolve_context function to pass to RenderEngine.__init__().
"""
if self._is_missing_tags_strict():
return context_get
# Otherwise, ignore missing tags.
def resolve_context(stack, name):
try:
return context_get(stack, name)
except KeyNotFoundError:
return u''
return resolve_context
def _make_render_engine(self): def _make_render_engine(self):
""" """
Return a RenderEngine instance for rendering. Return a RenderEngine instance for rendering.
""" """
load_partial = self._make_load_partial() resolve_context = self._make_resolve_context()
resolve_partial = self._make_resolve_partial()
engine = RenderEngine(load_partial=load_partial, engine = RenderEngine(literal=self._to_unicode_hard,
literal=self._to_unicode_hard, escape=self._escape_to_unicode,
escape=self._escape_to_unicode) resolve_context=resolve_context,
resolve_partial=resolve_partial)
return engine return engine
# TODO: add unit tests for this method. # TODO: add unit tests for this method.
......
...@@ -8,10 +8,8 @@ Unit tests of context.py. ...@@ -8,10 +8,8 @@ Unit tests of context.py.
from datetime import datetime from datetime import datetime
import unittest import unittest
from pystache.context import _NOT_FOUND from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack
from pystache.context import _get_value from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable
from pystache.context import ContextStack
from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
class SimpleObject(object): class SimpleObject(object):
...@@ -39,7 +37,7 @@ class DictLike(object): ...@@ -39,7 +37,7 @@ class DictLike(object):
return self._dict[key] return self._dict[key]
class GetValueTests(unittest.TestCase, AssertIsMixin): class GetValueTestCase(unittest.TestCase, AssertIsMixin):
"""Test context._get_value().""" """Test context._get_value()."""
...@@ -224,7 +222,8 @@ class GetValueTests(unittest.TestCase, AssertIsMixin): ...@@ -224,7 +222,8 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop') self.assertNotFound(item2, 'pop')
class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): class ContextStackTestCase(unittest.TestCase, AssertIsMixin, AssertStringMixin,
AssertExceptionMixin):
""" """
Test the ContextStack class. Test the ContextStack class.
...@@ -326,6 +325,24 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -326,6 +325,24 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
context = ContextStack.create({'foo': 'bar'}, foo='buzz') context = ContextStack.create({'foo': 'bar'}, foo='buzz')
self.assertEqual(context.get('foo'), 'buzz') self.assertEqual(context.get('foo'), 'buzz')
## Test the get() method.
def test_get__single_dot(self):
"""
Test getting a single dot (".").
"""
context = ContextStack("a", "b")
self.assertEqual(context.get("."), "b")
def test_get__single_dot__missing(self):
"""
Test getting a single dot (".") with an empty context stack.
"""
context = ContextStack()
self.assertException(KeyNotFoundError, "Key '.' not found: empty context stack", context.get, ".")
def test_get__key_present(self): def test_get__key_present(self):
""" """
Test getting a key. Test getting a key.
...@@ -340,15 +357,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -340,15 +357,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
""" """
context = ContextStack() context = ContextStack()
self.assertString(context.get("foo"), u'') self.assertException(KeyNotFoundError, "Key 'foo' not found: first part", context.get, "foo")
def test_get__default(self):
"""
Test that get() respects the default value.
"""
context = ContextStack()
self.assertEqual(context.get("foo", "bar"), "bar")
def test_get__precedence(self): def test_get__precedence(self):
""" """
...@@ -444,10 +453,10 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -444,10 +453,10 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
def test_dot_notation__missing_attr_or_key(self): def test_dot_notation__missing_attr_or_key(self):
name = "foo.bar.baz.bak" name = "foo.bar.baz.bak"
stack = ContextStack({"foo": {"bar": {}}}) stack = ContextStack({"foo": {"bar": {}}})
self.assertString(stack.get(name), u'') self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
stack = ContextStack({"foo": Attachable(bar=Attachable())}) stack = ContextStack({"foo": Attachable(bar=Attachable())})
self.assertString(stack.get(name), u'') self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
def test_dot_notation__missing_part_terminates_search(self): def test_dot_notation__missing_part_terminates_search(self):
""" """
...@@ -471,7 +480,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin): ...@@ -471,7 +480,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
""" """
stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'}) stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'})
self.assertEqual(stack.get('a'), 'A') self.assertEqual(stack.get('a'), 'A')
self.assertString(stack.get('a.b'), u'') self.assertException(KeyNotFoundError, "Key 'a.b' not found: missing 'b'", stack.get, "a.b")
stack.pop() stack.pop()
self.assertEqual(stack.get('a.b'), 'A.B') self.assertEqual(stack.get('a.b'), 'A.B')
......
...@@ -7,11 +7,11 @@ Unit tests of renderengine.py. ...@@ -7,11 +7,11 @@ Unit tests of renderengine.py.
import unittest import unittest
from pystache.context import ContextStack from pystache.context import ContextStack, KeyNotFoundError
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 context_get, RenderEngine
from pystache.tests.common import AssertStringMixin, Attachable from pystache.tests.common import AssertStringMixin, AssertExceptionMixin, Attachable
def mock_literal(s): def mock_literal(s):
...@@ -45,14 +45,14 @@ class RenderEngineTestCase(unittest.TestCase): ...@@ -45,14 +45,14 @@ class RenderEngineTestCase(unittest.TestCase):
""" """
# In real-life, these arguments would be functions # In real-life, these arguments would be functions
engine = RenderEngine(load_partial="foo", literal="literal", escape="escape") engine = RenderEngine(resolve_partial="foo", literal="literal", escape="escape")
self.assertEqual(engine.escape, "escape") self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal") self.assertEqual(engine.literal, "literal")
self.assertEqual(engine.load_partial, "foo") self.assertEqual(engine.resolve_partial, "foo")
class RenderTests(unittest.TestCase, AssertStringMixin): class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
""" """
Tests RenderEngine.render(). Tests RenderEngine.render().
...@@ -69,7 +69,10 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -69,7 +69,10 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
""" """
escape = defaults.TAG_ESCAPE escape = defaults.TAG_ESCAPE
engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
engine = RenderEngine(literal=unicode, escape=escape,
resolve_context=context_get,
resolve_partial=None)
return engine return engine
def _assert_render(self, expected, template, *context, **kwargs): def _assert_render(self, expected, template, *context, **kwargs):
...@@ -81,7 +84,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -81,7 +84,7 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
engine = kwargs.get('engine', self._engine()) engine = kwargs.get('engine', self._engine())
if partials is not None: if partials is not None:
engine.load_partial = lambda key: unicode(partials[key]) engine.resolve_partial = lambda key: unicode(partials[key])
context = ContextStack(*context) context = ContextStack(*context)
...@@ -92,14 +95,14 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -92,14 +95,14 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
def test_render(self): def test_render(self):
self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'}) self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
def test__load_partial(self): def test__resolve_partial(self):
""" """
Test that render() uses the load_template attribute. Test that render() uses the load_template attribute.
""" """
engine = self._engine() engine = self._engine()
partials = {'partial': u"{{person}}"} partials = {'partial': u"{{person}}"}
engine.load_partial = lambda key: partials[key] engine.resolve_partial = lambda key: partials[key]
self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine) self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
...@@ -594,33 +597,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -594,33 +597,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': person} context = {'person': person}
self._assert_render(u'Hello, Biggles. I see you are 42.', template, context) 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): def test_dot_notation__multiple_levels(self):
""" """
Test dot notation with multiple levels. Test dot notation with multiple levels.
""" """
template = """Hello, Mr. {{person.name.lastname}}. template = """Hello, Mr. {{person.name.lastname}}.
I see you're back from {{person.travels.last.country.city}}. 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. expected = u"""Hello, Mr. Pither.
I see you're back from Cornwall. I see you're back from Cornwall."""
I'm missing some of your details: ."""
context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'}, context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
'travels': {'last': {'country': {'city': 'Cornwall'}}}, 'travels': {'last': {'country': {'city': 'Cornwall'}}},
'details': {'public': 'likes cycling'}}} 'details': {'public': 'likes cycling'}}}
...@@ -652,6 +637,14 @@ class RenderTests(unittest.TestCase, AssertStringMixin): ...@@ -652,6 +637,14 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
https://github.com/mustache/spec/pull/48 https://github.com/mustache/spec/pull/48
""" """
template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} } context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} }
self._assert_render(u'A.B :: (A :: )', template, context)
template = '{{a.b}}'
self._assert_render(u'A.B', template, context)
template = '{{#c}}{{a}}{{/c}}'
self._assert_render(u'A', template, context)
template = '{{#c}}{{a.b}}{{/c}}'
self.assertException(KeyNotFoundError, "Key u'a.b' not found: missing u'b'",
self._assert_render, u'A.B :: (A :: )', template, context)
...@@ -14,6 +14,7 @@ from examples.simple import Simple ...@@ -14,6 +14,7 @@ from examples.simple import Simple
from pystache import Renderer from pystache import Renderer
from pystache import TemplateSpec from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError from pystache.common import TemplateNotFoundError
from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader from pystache.loader import Loader
from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin
...@@ -124,6 +125,22 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -124,6 +125,22 @@ class RendererInitTestCase(unittest.TestCase):
renderer = Renderer(file_extension='foo') renderer = Renderer(file_extension='foo')
self.assertEqual(renderer.file_extension, 'foo') self.assertEqual(renderer.file_extension, 'foo')
def test_missing_tags(self):
"""
Check that the missing_tags attribute is set correctly.
"""
renderer = Renderer(missing_tags='foo')
self.assertEqual(renderer.missing_tags, 'foo')
def test_missing_tags__default(self):
"""
Check the missing_tags default.
"""
renderer = Renderer()
self.assertEqual(renderer.missing_tags, 'ignore')
def test_search_dirs__default(self): def test_search_dirs__default(self):
""" """
Check the search_dirs default. Check the search_dirs default.
...@@ -319,37 +336,37 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -319,37 +336,37 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.string_encoding = 'utf_8' renderer.string_encoding = 'utf_8'
self.assertEqual(renderer.render(template), u"déf") self.assertEqual(renderer.render(template), u"déf")
def test_make_load_partial(self): def test_make_resolve_partial(self):
""" """
Test the _make_load_partial() method. Test the _make_resolve_partial() method.
""" """
renderer = Renderer() renderer = Renderer()
renderer.partials = {'foo': 'bar'} renderer.partials = {'foo': 'bar'}
load_partial = renderer._make_load_partial() resolve_partial = renderer._make_resolve_partial()
actual = load_partial('foo') actual = resolve_partial('foo')
self.assertEqual(actual, 'bar') self.assertEqual(actual, 'bar')
self.assertEqual(type(actual), unicode, "RenderEngine requires that " self.assertEqual(type(actual), unicode, "RenderEngine requires that "
"load_partial return unicode strings.") "resolve_partial return unicode strings.")
def test_make_load_partial__unicode(self): def test_make_resolve_partial__unicode(self):
""" """
Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode. Test _make_resolve_partial(): that resolve_partial doesn't "double-decode" Unicode.
""" """
renderer = Renderer() renderer = Renderer()
renderer.partials = {'partial': 'foo'} renderer.partials = {'partial': 'foo'}
load_partial = renderer._make_load_partial() resolve_partial = renderer._make_resolve_partial()
self.assertEqual(load_partial("partial"), "foo") self.assertEqual(resolve_partial("partial"), "foo")
# Now with a value that is already unicode. # Now with a value that is already unicode.
renderer.partials = {'partial': u'foo'} renderer.partials = {'partial': u'foo'}
load_partial = renderer._make_load_partial() resolve_partial = renderer._make_resolve_partial()
# If the next line failed, we would get the following error: # If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported # TypeError: decoding Unicode is not supported
self.assertEqual(load_partial("partial"), "foo") self.assertEqual(resolve_partial("partial"), "foo")
def test_render_path(self): def test_render_path(self):
""" """
...@@ -406,7 +423,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin): ...@@ -406,7 +423,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
# we no longer need to exercise all rendering code paths through # we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the # the Renderer. It suffices to test rendering paths through the
# RenderEngine for the same amount of code coverage. # RenderEngine for the same amount of code coverage.
class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin): class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
""" """
Check the RenderEngine returned by Renderer._make_render_engine(). Check the RenderEngine returned by Renderer._make_render_engine().
...@@ -420,11 +437,11 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin): ...@@ -420,11 +437,11 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
""" """
return _make_renderer() return _make_renderer()
## Test the engine's load_partial attribute. ## Test the engine's resolve_partial attribute.
def test__load_partial__returns_unicode(self): def test__resolve_partial__returns_unicode(self):
""" """
Check that load_partial returns unicode (and not a subclass). Check that resolve_partial returns unicode (and not a subclass).
""" """
class MyUnicode(unicode): class MyUnicode(unicode):
...@@ -436,43 +453,70 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin): ...@@ -436,43 +453,70 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
actual = engine.load_partial('str') actual = engine.resolve_partial('str')
self.assertEqual(actual, "foo") self.assertEqual(actual, "foo")
self.assertEqual(type(actual), unicode) self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved. # Check that unicode subclasses are not preserved.
actual = engine.load_partial('subclass') actual = engine.resolve_partial('subclass')
self.assertEqual(actual, "abc") self.assertEqual(actual, "abc")
self.assertEqual(type(actual), unicode) self.assertEqual(type(actual), unicode)
def test__load_partial__not_found__default(self): def test__resolve_partial__not_found(self):
"""
Check that resolve_partial returns the empty string when a template is not found.
""" """
Check that load_partial provides a nice message when a template is not found. renderer = Renderer()
engine = renderer._make_render_engine()
resolve_partial = engine.resolve_partial
self.assertString(resolve_partial('foo'), u'')
def test__resolve_partial__not_found__missing_tags_strict(self):
"""
Check that resolve_partial provides a nice message when a template is not found.
""" """
renderer = Renderer() renderer = Renderer()
renderer.missing_tags = 'strict'
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
load_partial = engine.load_partial resolve_partial = engine.resolve_partial
self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']", self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']",
load_partial, "foo") resolve_partial, "foo")
def test__resolve_partial__not_found__partials_dict(self):
"""
Check that resolve_partial returns the empty string when a template is not found.
"""
renderer = Renderer()
renderer.partials = {}
engine = renderer._make_render_engine()
resolve_partial = engine.resolve_partial
def test__load_partial__not_found__dict(self): self.assertString(resolve_partial('foo'), u'')
def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self):
""" """
Check that load_partial provides a nice message when a template is not found. Check that resolve_partial provides a nice message when a template is not found.
""" """
renderer = Renderer() renderer = Renderer()
renderer.missing_tags = 'strict'
renderer.partials = {} renderer.partials = {}
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
load_partial = engine.load_partial resolve_partial = engine.resolve_partial
# Include dict directly since str(dict) is different in Python 2 and 3: # Include dict directly since str(dict) is different in Python 2 and 3:
# <type 'dict'> versus <class 'dict'>, respectively. # <type 'dict'> versus <class 'dict'>, respectively.
self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict, self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict,
load_partial, "foo") resolve_partial, "foo")
## Test the engine's literal attribute. ## Test the engine's literal attribute.
...@@ -595,3 +639,34 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin): ...@@ -595,3 +639,34 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
self.assertTrue(isinstance(s, unicode)) self.assertTrue(isinstance(s, unicode))
self.assertEqual(type(escape(s)), unicode) self.assertEqual(type(escape(s)), unicode)
## Test the engine's resolve_context attribute.
def test__resolve_context(self):
"""
Check resolve_context(): default arguments.
"""
renderer = Renderer()
engine = renderer._make_render_engine()
stack = ContextStack({'foo': 'bar'})
self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
self.assertString(u'', engine.resolve_context(stack, 'missing'))
def test__resolve_context__missing_tags_strict(self):
"""
Check resolve_context(): missing_tags 'strict'.
"""
renderer = Renderer()
renderer.missing_tags = 'strict'
engine = renderer._make_render_engine()
stack = ContextStack({'foo': 'bar'})
self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
self.assertException(KeyNotFoundError, "Key 'missing' not found: first part",
engine.resolve_context, stack, 'missing')
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