summaryrefslogtreecommitdiff
path: root/pystache/tests
diff options
context:
space:
mode:
Diffstat (limited to 'pystache/tests')
-rw-r--r--pystache/tests/common.py10
-rw-r--r--pystache/tests/data/locator/template.txt1
-rw-r--r--pystache/tests/doctesting.py6
-rw-r--r--pystache/tests/main.py104
-rw-r--r--pystache/tests/test___init__.py2
-rw-r--r--pystache/tests/test_context.py65
-rw-r--r--pystache/tests/test_defaults.py68
-rw-r--r--pystache/tests/test_loader.py17
-rw-r--r--pystache/tests/test_locator.py24
-rw-r--r--pystache/tests/test_parser.py3
-rw-r--r--pystache/tests/test_renderengine.py165
-rw-r--r--pystache/tests/test_renderer.py182
-rw-r--r--pystache/tests/test_specloader.py32
13 files changed, 542 insertions, 137 deletions
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
index 24b24dc..99be4c8 100644
--- a/pystache/tests/common.py
+++ b/pystache/tests/common.py
@@ -22,7 +22,7 @@ PROJECT_DIR = os.path.join(PACKAGE_DIR, '..')
SPEC_TEST_DIR = os.path.join(PROJECT_DIR, 'ext', 'spec', 'specs')
# TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files)
# containing doctests. The paths should be relative to the project directory.
-TEXT_DOCTEST_PATHS = ['README.rst']
+TEXT_DOCTEST_PATHS = ['README.md']
UNITTEST_FILE_PREFIX = "test_"
@@ -43,7 +43,10 @@ def html_escape(u):
return u.replace("'", ''')
-def get_data_path(file_name):
+def get_data_path(file_name=None):
+ """Return the path to a file in the test data directory."""
+ if file_name is None:
+ file_name = ""
return os.path.join(DATA_DIR, file_name)
@@ -139,8 +142,7 @@ class AssertStringMixin:
format = "%s"
# Show both friendly and literal versions.
- details = """String mismatch: %%s\
-
+ details = """String mismatch: %%s
Expected: \"""%s\"""
Actual: \"""%s\"""
diff --git a/pystache/tests/data/locator/template.txt b/pystache/tests/data/locator/template.txt
new file mode 100644
index 0000000..bef8160
--- /dev/null
+++ b/pystache/tests/data/locator/template.txt
@@ -0,0 +1 @@
+Test template file
diff --git a/pystache/tests/doctesting.py b/pystache/tests/doctesting.py
index 469c81e..1102b78 100644
--- a/pystache/tests/doctesting.py
+++ b/pystache/tests/doctesting.py
@@ -44,7 +44,11 @@ def get_doctests(text_file_dir):
paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS]
if sys.version_info >= (3,):
- paths = _convert_paths(paths)
+ # Skip the README doctests in Python 3 for now because examples
+ # rendering to unicode do not give consistent results
+ # (e.g. 'foo' vs u'foo').
+ # paths = _convert_paths(paths)
+ paths = []
suites = []
diff --git a/pystache/tests/main.py b/pystache/tests/main.py
index de56c44..184122d 100644
--- a/pystache/tests/main.py
+++ b/pystache/tests/main.py
@@ -1,7 +1,7 @@
# coding: utf-8
"""
-Exposes a run_tests() function that runs all tests in the project.
+Exposes a main() function that runs all tests in the project.
This module is for our test console script.
@@ -10,7 +10,7 @@ This module is for our test console script.
import os
import sys
import unittest
-from unittest import TestProgram
+from unittest import TestCase, TestProgram
import pystache
from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
@@ -24,6 +24,58 @@ from pystache.tests.spectesting import get_spec_tests
FROM_SOURCE_OPTION = "--from-source"
+def make_extra_tests(text_doctest_dir, spec_test_dir):
+ tests = []
+
+ if text_doctest_dir is not None:
+ doctest_suites = get_doctests(text_doctest_dir)
+ tests.extend(doctest_suites)
+
+ if spec_test_dir is not None:
+ spec_testcases = get_spec_tests(spec_test_dir)
+ tests.extend(spec_testcases)
+
+ return unittest.TestSuite(tests)
+
+
+def make_test_program_class(extra_tests):
+ """
+ Return a subclass of unittest.TestProgram.
+
+ """
+ # The function unittest.main() is an alias for unittest.TestProgram's
+ # constructor. TestProgram's constructor does the following:
+ #
+ # 1. calls self.parseArgs(argv),
+ # 2. which in turn calls self.createTests().
+ # 3. then the constructor calls self.runTests().
+ #
+ # The createTests() method sets the self.test attribute by calling one
+ # of self.testLoader's "loadTests" methods. Each loadTest method returns
+ # a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
+ # instance prior to calling runTests().
+ class PystacheTestProgram(TestProgram):
+
+ """
+ Instantiating an instance of this class runs all tests.
+
+ """
+
+ def createTests(self):
+ """
+ Load tests and set self.test to a unittest.TestSuite instance
+
+ Compare--
+
+ http://docs.python.org/library/unittest.html#unittest.TestSuite
+
+ """
+ super(PystacheTestProgram, self).createTests()
+ self.test.addTests(extra_tests)
+
+ return PystacheTestProgram
+
+
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def main(sys_argv):
@@ -52,7 +104,14 @@ def main(sys_argv):
sys_argv.pop(1)
except IndexError:
if should_source_exist:
- spec_test_dir = SPEC_TEST_DIR
+ if not os.path.exists(SPEC_TEST_DIR):
+ # Then the user is probably using a downloaded sdist rather
+ # than a repository clone (since the sdist does not include
+ # the spec test directory).
+ print("pystache: skipping spec tests: spec test directory "
+ "not found")
+ else:
+ spec_test_dir = SPEC_TEST_DIR
try:
# TODO: use optparse command options instead.
@@ -71,16 +130,17 @@ def main(sys_argv):
# Add the current module for unit tests contained here.
sys_argv.append(__name__)
- _PystacheTestProgram._text_doctest_dir = project_dir
- _PystacheTestProgram._spec_test_dir = spec_test_dir
SetupTests.project_dir = project_dir
+ extra_tests = make_extra_tests(project_dir, spec_test_dir)
+ test_program_class = make_test_program_class(extra_tests)
+
# We pass None for the module because we do not want the unittest
# module to resolve module names relative to a given module.
# (This would require importing all of the unittest modules from
# this module.) See the loadTestsFromName() method of the
# unittest.TestLoader class for more details on this parameter.
- _PystacheTestProgram(argv=sys_argv, module=None)
+ test_program_class(argv=sys_argv, module=None)
# No need to return since unitttest.main() exits.
@@ -103,7 +163,7 @@ def _discover_test_modules(package_dir):
return names
-class SetupTests(unittest.TestCase):
+class SetupTests(TestCase):
"""Tests about setup.py."""
@@ -123,33 +183,3 @@ class SetupTests(unittest.TestCase):
self.assertEqual(VERSION, pystache.__version__)
finally:
sys.path = original_path
-
-
-# The function unittest.main() is an alias for unittest.TestProgram's
-# constructor. TestProgram's constructor calls self.runTests() as its
-# final step, which expects self.test to be set. The constructor sets
-# the self.test attribute by calling one of self.testLoader's "loadTests"
-# methods prior to callint self.runTests(). Each loadTest method returns
-# a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
-# instance prior to calling runTests().
-class _PystacheTestProgram(TestProgram):
-
- """
- Instantiating an instance of this class runs all tests.
-
- """
-
- def runTests(self):
- # self.test is a unittest.TestSuite instance:
- # http://docs.python.org/library/unittest.html#unittest.TestSuite
- tests = self.test
-
- if self._text_doctest_dir is not None:
- doctest_suites = get_doctests(self._text_doctest_dir)
- tests.addTests(doctest_suites)
-
- if self._spec_test_dir is not None:
- spec_testcases = get_spec_tests(self._spec_test_dir)
- tests.addTests(spec_testcases)
-
- TestProgram.runTests(self)
diff --git a/pystache/tests/test___init__.py b/pystache/tests/test___init__.py
index d4f3526..eae42c1 100644
--- a/pystache/tests/test___init__.py
+++ b/pystache/tests/test___init__.py
@@ -23,7 +23,7 @@ class InitTests(unittest.TestCase):
"""
actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
- expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
+ expected = set(['parse', 'render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
self.assertEqual(actual, expected)
diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py
index d432428..238e4b0 100644
--- a/pystache/tests/test_context.py
+++ b/pystache/tests/test_context.py
@@ -8,10 +8,8 @@ Unit tests of context.py.
from datetime import datetime
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, Attachable
+from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack
+from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable
class SimpleObject(object):
@@ -39,7 +37,7 @@ class DictLike(object):
return self._dict[key]
-class GetValueTests(unittest.TestCase, AssertIsMixin):
+class GetValueTestCase(unittest.TestCase, AssertIsMixin):
"""Test context._get_value()."""
@@ -147,6 +145,26 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertEqual(item["foo"], "bar")
self.assertNotFound(item, "foo")
+ def test_object__property__raising_exception(self):
+ """
+ Test getting a property that raises an exception.
+
+ """
+ class Foo(object):
+
+ @property
+ def bar(self):
+ return 1
+
+ @property
+ def baz(self):
+ raise ValueError("test")
+
+ foo = Foo()
+ self.assertEqual(_get_value(foo, 'bar'), 1)
+ self.assertNotFound(foo, 'missing')
+ self.assertRaises(ValueError, _get_value, foo, 'baz')
+
### Case: the item is an instance of a built-in type.
def test_built_in_type__integer(self):
@@ -204,7 +222,8 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop')
-class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class ContextStackTestCase(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertExceptionMixin):
"""
Test the ContextStack class.
@@ -306,6 +325,24 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
context = ContextStack.create({'foo': 'bar'}, 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):
"""
Test getting a key.
@@ -320,15 +357,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
context = ContextStack()
- self.assertString(context.get("foo"), u'')
-
- def test_get__default(self):
- """
- Test that get() respects the default value.
-
- """
- context = ContextStack()
- self.assertEqual(context.get("foo", "bar"), "bar")
+ self.assertException(KeyNotFoundError, "Key 'foo' not found: first part", context.get, "foo")
def test_get__precedence(self):
"""
@@ -424,10 +453,10 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
def test_dot_notation__missing_attr_or_key(self):
name = "foo.bar.baz.bak"
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())})
- 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):
"""
@@ -451,7 +480,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
stack = ContextStack({'a': {'b': 'A.B'}}, {'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()
self.assertEqual(stack.get('a.b'), 'A.B')
diff --git a/pystache/tests/test_defaults.py b/pystache/tests/test_defaults.py
new file mode 100644
index 0000000..c78ea7c
--- /dev/null
+++ b/pystache/tests/test_defaults.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+
+"""
+Unit tests for defaults.py.
+
+"""
+
+import unittest
+
+import pystache
+
+from pystache.tests.common import AssertStringMixin
+
+
+# TODO: make sure each default has at least one test.
+class DefaultsConfigurableTestCase(unittest.TestCase, AssertStringMixin):
+
+ """Tests that the user can change the defaults at runtime."""
+
+ # TODO: switch to using a context manager after 2.4 is deprecated.
+ def setUp(self):
+ """Save the defaults."""
+ defaults = [
+ 'DECODE_ERRORS', 'DELIMITERS',
+ 'FILE_ENCODING', 'MISSING_TAGS',
+ 'SEARCH_DIRS', 'STRING_ENCODING',
+ 'TAG_ESCAPE', 'TEMPLATE_EXTENSION'
+ ]
+ self.saved = {}
+ for e in defaults:
+ self.saved[e] = getattr(pystache.defaults, e)
+
+ def tearDown(self):
+ for key, value in self.saved.items():
+ setattr(pystache.defaults, key, value)
+
+ def test_tag_escape(self):
+ """Test that changes to defaults.TAG_ESCAPE take effect."""
+ template = u"{{foo}}"
+ context = {'foo': '<'}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"&lt;")
+
+ pystache.defaults.TAG_ESCAPE = lambda u: u
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"<")
+
+ def test_delimiters(self):
+ """Test that changes to defaults.DELIMITERS take effect."""
+ template = u"[[foo]]{{foo}}"
+ context = {'foo': 'FOO'}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"[[foo]]FOO")
+
+ pystache.defaults.DELIMITERS = ('[[', ']]')
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"FOO{{foo}}")
+
+ def test_missing_tags(self):
+ """Test that changes to defaults.MISSING_TAGS take effect."""
+ template = u"{{foo}}"
+ context = {}
+ actual = pystache.render(template, context)
+ self.assertString(actual, u"")
+
+ pystache.defaults.MISSING_TAGS = 'strict'
+ self.assertRaises(pystache.context.KeyNotFoundError,
+ pystache.render, template, context)
diff --git a/pystache/tests/test_loader.py b/pystache/tests/test_loader.py
index c47239c..f2c2187 100644
--- a/pystache/tests/test_loader.py
+++ b/pystache/tests/test_loader.py
@@ -14,6 +14,10 @@ from pystache import defaults
from pystache.loader import Loader
+# We use the same directory as the locator tests for now.
+LOADER_DATA_DIR = os.path.join(DATA_DIR, 'locator')
+
+
class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
def setUp(self):
@@ -178,7 +182,7 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
actual = loader.read(path, encoding='utf-8')
self.assertString(actual, u'non-ascii: é')
- def test_loader__to_unicode__attribute(self):
+ def test_read__to_unicode__attribute(self):
"""
Test read(): to_unicode attribute respected.
@@ -192,3 +196,14 @@ class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
#actual = loader.read(path)
#self.assertString(actual, u'non-ascii: ')
+ def test_load_file(self):
+ loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR])
+ template = loader.load_file('template.txt')
+ self.assertEqual(template, 'Test template file\n')
+
+ def test_load_name(self):
+ loader = Loader(search_dirs=[DATA_DIR, LOADER_DATA_DIR],
+ extension='txt')
+ template = loader.load_name('template')
+ self.assertEqual(template, 'Test template file\n')
+
diff --git a/pystache/tests/test_locator.py b/pystache/tests/test_locator.py
index f17a289..ee1c2ff 100644
--- a/pystache/tests/test_locator.py
+++ b/pystache/tests/test_locator.py
@@ -19,6 +19,9 @@ from pystache.tests.common import DATA_DIR, EXAMPLES_DIR, AssertExceptionMixin
from pystache.tests.data.views import SayHello
+LOCATOR_DATA_DIR = os.path.join(DATA_DIR, 'locator')
+
+
class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def _locator(self):
@@ -53,7 +56,17 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def test_get_object_directory__not_hasattr_module(self):
locator = Locator()
- obj = datetime(2000, 1, 1)
+ # Previously, we used a genuine object -- a datetime instance --
+ # because datetime instances did not have the __module__ attribute
+ # in CPython. See, for example--
+ #
+ # http://bugs.python.org/issue15223
+ #
+ # However, since datetime instances do have the __module__ attribute
+ # in PyPy, we needed to switch to something else once we added
+ # support for PyPi. This was so that our test runs would pass
+ # in all systems.
+ obj = "abc"
self.assertFalse(hasattr(obj, '__module__'))
self.assertEqual(locator.get_object_directory(obj), None)
@@ -77,6 +90,13 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
+ def test_find_file(self):
+ locator = Locator()
+ path = locator.find_file('template.txt', [LOCATOR_DATA_DIR])
+
+ expected_path = os.path.join(LOCATOR_DATA_DIR, 'template.txt')
+ self.assertEqual(path, expected_path)
+
def test_find_name(self):
locator = Locator()
path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
@@ -97,7 +117,7 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
locator = Locator()
dir1 = DATA_DIR
- dir2 = os.path.join(DATA_DIR, 'locator')
+ dir2 = LOCATOR_DATA_DIR
self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
index 4aa0959..92248ea 100644
--- a/pystache/tests/test_parser.py
+++ b/pystache/tests/test_parser.py
@@ -7,6 +7,7 @@ Unit tests of parser.py.
import unittest
+from pystache.defaults import DELIMITERS
from pystache.parser import _compile_template_re as make_re
@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary.
"""
- re = make_re()
+ re = make_re(DELIMITERS)
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
index b13e246..db916f7 100644
--- a/pystache/tests/test_renderengine.py
+++ b/pystache/tests/test_renderengine.py
@@ -5,13 +5,23 @@ Unit tests of renderengine.py.
"""
+import sys
import unittest
-from pystache.context import ContextStack
+from pystache.context import ContextStack, KeyNotFoundError
from pystache import defaults
from pystache.parser import ParsingError
-from pystache.renderengine import RenderEngine
-from pystache.tests.common import AssertStringMixin, Attachable
+from pystache.renderer import Renderer
+from pystache.renderengine import context_get, RenderEngine
+from pystache.tests.common import AssertStringMixin, AssertExceptionMixin, Attachable
+
+
+def _get_unicode_char():
+ if sys.version_info < (3, ):
+ return 'u'
+ return ''
+
+_UNICODE_CHAR = _get_unicode_char()
def mock_literal(s):
@@ -45,14 +55,16 @@ class RenderEngineTestCase(unittest.TestCase):
"""
# 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", to_str="str")
self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal")
- self.assertEqual(engine.load_partial, "foo")
+ self.assertEqual(engine.resolve_partial, "foo")
+ self.assertEqual(engine.to_str, "str")
-class RenderTests(unittest.TestCase, AssertStringMixin):
+class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
"""
Tests RenderEngine.render().
@@ -68,8 +80,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Create and return a default RenderEngine for testing.
"""
- escape = defaults.TAG_ESCAPE
- engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ renderer = Renderer(string_encoding='utf-8', missing_tags='strict')
+ engine = renderer._make_render_engine()
+
return engine
def _assert_render(self, expected, template, *context, **kwargs):
@@ -81,25 +94,26 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
engine = kwargs.get('engine', self._engine())
if partials is not None:
- engine.load_partial = lambda key: unicode(partials[key])
+ engine.resolve_partial = lambda key: unicode(partials[key])
context = ContextStack(*context)
- actual = engine.render(template, context)
+ # RenderEngine.render() only accepts unicode template strings.
+ actual = engine.render(unicode(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):
+ def test__resolve_partial(self):
"""
Test that render() uses the load_template attribute.
"""
engine = self._engine()
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)
@@ -170,6 +184,47 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(u'**bar bar**', template, context, engine=engine)
+ # Custom to_str for testing purposes.
+ def _to_str(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ def test_to_str(self):
+ """Test the to_str attribute."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__lambda(self):
+ """Test the to_str attribute for a lambda."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': lambda: None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_list(self):
+ """Test the to_str attribute for a section list."""
+ engine = self._engine()
+ template = '{{#list}}{{.}}{{/list}}'
+ context = {'list': [None, None]}
+
+ self._assert_render(u'NoneNone', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_lambda(self):
+ # TODO: add a test for a "method with an arity of 1".
+ pass
+
def test__non_basestring__literal_and_escaped(self):
"""
Test a context value that is not a basestring instance.
@@ -285,6 +340,16 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': item, attr_name: 7}
self._assert_render(u'7', template, context)
+ # This test is also important for testing 2to3.
+ def test_interpolation__nonascii_nonunicode(self):
+ """
+ Test a tag whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
def test_implicit_iterator__literal(self):
"""
Test an implicit iterator in a literal tag.
@@ -343,6 +408,28 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+ ## Test cases related specifically to lambdas.
+
+ # This test is also important for testing 2to3.
+ def test_section__nonascii_nonunicode(self):
+ """
+ Test a section whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{#nonascii}}{{.}}{{/nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ # This test is also important for testing 2to3.
+ def test_lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda tag value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{lambda}}'
+ context = {'lambda': lambda: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
## Test cases related specifically to sections.
def test_section__end_tag_with_no_start_tag(self):
@@ -461,6 +548,25 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': (lambda text: 'Hi %s' % text)}
self._assert_render(u'Hi Mom', template, context)
+ # This test is also important for testing 2to3.
+ def test_section__lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda section value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{#lambda}}{{/lambda}}'
+ context = {'lambda': lambda text: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ def test_section__lambda__returning_nonstring(self):
+ """
+ Test a lambda section value returning a non-string.
+
+ """
+ template = '{{#lambda}}foo{{/lambda}}'
+ context = {'lambda': lambda text: len(text)}
+ self._assert_render(u'3', template, context)
+
def test_section__iterable(self):
"""
Check that objects supporting iteration (aside from dicts) behave like lists.
@@ -609,33 +715,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
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}}."""
+ I see you're back from {{person.travels.last.country.city}}."""
expected = u"""Hello, Mr. Pither.
- I see you're back from Cornwall.
- I'm missing some of your details: ."""
+ I see you're back from Cornwall."""
context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
'travels': {'last': {'country': {'city': 'Cornwall'}}},
'details': {'public': 'likes cycling'}}}
@@ -667,6 +755,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
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)
+
+ 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 %(unicode)s'a.b' not found: missing %(unicode)s'b'" %
+ {'unicode': _UNICODE_CHAR},
+ self._assert_render, 'A.B :: (A :: )', template, context)
diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py
index f04c799..0dbe0d9 100644
--- a/pystache/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -14,6 +14,7 @@ from examples.simple import Simple
from pystache import Renderer
from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError
+from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader
from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin
@@ -124,6 +125,22 @@ class RendererInitTestCase(unittest.TestCase):
renderer = 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):
"""
Check the search_dirs default.
@@ -319,37 +336,44 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.string_encoding = 'utf_8'
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.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(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.partials = {'partial': 'foo'}
- load_partial = renderer._make_load_partial()
- self.assertEqual(load_partial("partial"), "foo")
+ resolve_partial = renderer._make_resolve_partial()
+ self.assertEqual(resolve_partial("partial"), "foo")
# Now with a value that is already unicode.
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:
# TypeError: decoding Unicode is not supported
- self.assertEqual(load_partial("partial"), "foo")
+ self.assertEqual(resolve_partial("partial"), "foo")
+
+ def test_render_name(self):
+ """Test the render_name() method."""
+ data_dir = get_data_path()
+ renderer = Renderer(search_dirs=data_dir)
+ actual = renderer.render_name("say_hello", to='foo')
+ self.assertString(actual, u"Hello, foo")
def test_render_path(self):
"""
@@ -401,12 +425,45 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
actual = renderer.render(view)
self.assertEqual('Hi pizza!', actual)
+ def test_custom_string_coercion_via_assignment(self):
+ """
+ Test that string coercion can be customized via attribute assignment.
+
+ """
+ renderer = self._renderer()
+ def to_str(val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ self.assertEqual(renderer.render('{{value}}', value=None), 'None')
+ renderer.str_coerce = to_str
+ self.assertEqual(renderer.render('{{value}}', value=None), '')
+
+ def test_custom_string_coercion_via_subclassing(self):
+ """
+ Test that string coercion can be customized via subclassing.
+
+ """
+ class MyRenderer(Renderer):
+ def str_coerce(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+ renderer1 = Renderer()
+ renderer2 = MyRenderer()
+
+ self.assertEqual(renderer1.render('{{value}}', value=None), 'None')
+ self.assertEqual(renderer2.render('{{value}}', value=None), '')
+
# By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the
# 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().
@@ -420,11 +477,11 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
"""
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):
@@ -436,43 +493,70 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
engine = renderer._make_render_engine()
- actual = engine.load_partial('str')
+ actual = engine.resolve_partial('str')
self.assertEqual(actual, "foo")
self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved.
- actual = engine.load_partial('subclass')
+ actual = engine.resolve_partial('subclass')
self.assertEqual(actual, "abc")
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.
+
+ """
+ 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 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.missing_tags = 'strict'
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: ['.']",
- load_partial, "foo")
+ resolve_partial, "foo")
- def test__load_partial__not_found__dict(self):
+ def test__resolve_partial__not_found__partials_dict(self):
"""
- Check that load_partial provides a nice message when a template is not found.
+ Check that resolve_partial returns the empty string when a template is not found.
"""
renderer = Renderer()
renderer.partials = {}
engine = renderer._make_render_engine()
- load_partial = engine.load_partial
+ resolve_partial = engine.resolve_partial
+
+ self.assertString(resolve_partial('foo'), u'')
+
+ def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self):
+ """
+ Check that resolve_partial provides a nice message when a template is not found.
- # Include dict directly since str(dict) is different in Python 2 and 3:
- # <type 'dict'> versus <class 'dict'>, respectively.
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'strict'
+ renderer.partials = {}
+
+ engine = renderer._make_render_engine()
+ resolve_partial = engine.resolve_partial
+
+ # Include dict directly since str(dict) is different in Python 2 and 3:
+ # <type 'dict'> versus <class 'dict'>, respectively.
self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict,
- load_partial, "foo")
+ resolve_partial, "foo")
## Test the engine's literal attribute.
@@ -595,3 +679,47 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
self.assertTrue(isinstance(s, unicode))
self.assertEqual(type(escape(s)), unicode)
+ ## Test the missing_tags attribute.
+
+ def test__missing_tags__unknown_value(self):
+ """
+ Check missing_tags attribute: setting an unknown value.
+
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'foo'
+
+ self.assertException(Exception, "Unsupported 'missing_tags' value: 'foo'",
+ renderer._make_render_engine)
+
+ ## 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')
diff --git a/pystache/tests/test_specloader.py b/pystache/tests/test_specloader.py
index 24fb34d..d934987 100644
--- a/pystache/tests/test_specloader.py
+++ b/pystache/tests/test_specloader.py
@@ -30,6 +30,14 @@ class Thing(object):
pass
+class AssertPathsMixin:
+
+ """A unittest.TestCase mixin to check path equality."""
+
+ def assertPaths(self, actual, expected):
+ self.assertEqual(actual, expected)
+
+
class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_template_rel_directory(self):
@@ -174,7 +182,8 @@ def _make_specloader():
return SpecLoader(loader=loader)
-class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertPathsMixin):
"""
Tests template_spec.SpecLoader.
@@ -288,13 +297,21 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
self.assertEqual(loader.s, "template-foo")
self.assertEqual(loader.encoding, "encoding-foo")
+ def test_find__template_path(self):
+ """Test _find() with TemplateSpec.template_path."""
+ loader = self._make_specloader()
+ custom = TemplateSpec()
+ custom.template_path = "path/foo"
+ actual = loader._find(custom)
+ self.assertPaths(actual, "path/foo")
+
# TODO: migrate these tests into the SpecLoaderTests class.
# TODO: rename the get_template() tests to test load().
# TODO: condense, reorganize, and rename the tests so that it is
# clear whether we have full test coverage (e.g. organized by
# TemplateSpec attributes or something).
-class TemplateSpecTests(unittest.TestCase):
+class TemplateSpecTests(unittest.TestCase, AssertPathsMixin):
def _make_loader(self):
return _make_specloader()
@@ -358,13 +375,6 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.txt'))
- def _assert_paths(self, actual, expected):
- """
- Assert that two paths are the same.
-
- """
- self.assertEqual(actual, expected)
-
def test_find__with_directory(self):
"""
Test _find() with a view that has a directory specified.
@@ -379,7 +389,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'foo/bar.txt')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def test_find__without_directory(self):
"""
@@ -394,7 +404,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def _assert_get_template(self, custom, expected):
loader = self._make_loader()