diff options
Diffstat (limited to 'pystache/tests')
-rw-r--r-- | pystache/tests/common.py | 10 | ||||
-rw-r--r-- | pystache/tests/data/locator/template.txt | 1 | ||||
-rw-r--r-- | pystache/tests/doctesting.py | 6 | ||||
-rw-r--r-- | pystache/tests/main.py | 104 | ||||
-rw-r--r-- | pystache/tests/test___init__.py | 2 | ||||
-rw-r--r-- | pystache/tests/test_context.py | 65 | ||||
-rw-r--r-- | pystache/tests/test_defaults.py | 68 | ||||
-rw-r--r-- | pystache/tests/test_loader.py | 17 | ||||
-rw-r--r-- | pystache/tests/test_locator.py | 24 | ||||
-rw-r--r-- | pystache/tests/test_parser.py | 3 | ||||
-rw-r--r-- | pystache/tests/test_renderengine.py | 165 | ||||
-rw-r--r-- | pystache/tests/test_renderer.py | 182 | ||||
-rw-r--r-- | pystache/tests/test_specloader.py | 32 |
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"<") + + 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: <', 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() |