summaryrefslogtreecommitdiff
path: root/pystache
diff options
context:
space:
mode:
authorRodrigo Bernardo Pimentel <rbp@isnomore.net>2012-04-29 18:16:19 +0200
committerRodrigo Bernardo Pimentel <rbp@isnomore.net>2012-04-29 18:16:19 +0200
commit012bdba1d29c37c3b0bbdd889ab7f6717c649856 (patch)
tree1fa4bbd1d32457ba1ef9eabe345e7ab7167d551f /pystache
parent544b7a35e6266bf0bc3d50168f6611d1ffc91f4a (diff)
parent54eb4b5047bc75a596c9734f460726aa462aea2e (diff)
downloadpystache-012bdba1d29c37c3b0bbdd889ab7f6717c649856.tar.gz
Merge branch 'development' of https://github.com/defunkt/pystache into development
Conflicts: pystache/context.py pystache/tests/test_context.py pystache/tests/test_renderengine.py tests/common.py
Diffstat (limited to 'pystache')
-rw-r--r--pystache/__init__.py13
-rw-r--r--pystache/commands/__init__.py4
-rw-r--r--pystache/commands/render.py (renamed from pystache/commands.py)21
-rw-r--r--pystache/commands/test.py18
-rw-r--r--pystache/common.py26
-rw-r--r--pystache/context.py68
-rw-r--r--pystache/defaults.py15
-rw-r--r--pystache/init.py3
-rw-r--r--pystache/loader.py31
-rw-r--r--pystache/parsed.py2
-rw-r--r--pystache/parser.py63
-rw-r--r--pystache/renderengine.py52
-rw-r--r--pystache/renderer.py49
-rw-r--r--pystache/specloader.py (renamed from pystache/spec_loader.py)0
-rw-r--r--pystache/template_spec.py6
-rw-r--r--pystache/tests/__init__.py4
-rwxr-xr-xpystache/tests/benchmark.py94
-rw-r--r--pystache/tests/common.py193
-rw-r--r--pystache/tests/data/__init__.py4
-rw-r--r--pystache/tests/data/ascii.mustache1
-rw-r--r--pystache/tests/data/duplicate.mustache1
-rw-r--r--pystache/tests/data/locator/__init__.py4
-rw-r--r--pystache/tests/data/locator/duplicate.mustache1
-rw-r--r--pystache/tests/data/non_ascii.mustache1
-rw-r--r--pystache/tests/data/sample_view.mustache1
-rw-r--r--pystache/tests/data/say_hello.mustache1
-rw-r--r--pystache/tests/data/views.py21
-rw-r--r--pystache/tests/doctesting.py90
-rw-r--r--pystache/tests/examples/__init__.py4
-rw-r--r--pystache/tests/examples/comments.mustache1
-rw-r--r--pystache/tests/examples/comments.py10
-rw-r--r--pystache/tests/examples/complex.mustache6
-rw-r--r--pystache/tests/examples/complex.py26
-rw-r--r--pystache/tests/examples/delimiters.mustache6
-rw-r--r--pystache/tests/examples/delimiters.py16
-rw-r--r--pystache/tests/examples/double_section.mustache3
-rw-r--r--pystache/tests/examples/double_section.py13
-rw-r--r--pystache/tests/examples/escaped.mustache1
-rw-r--r--pystache/tests/examples/escaped.py10
-rw-r--r--pystache/tests/examples/extensionless1
-rw-r--r--pystache/tests/examples/inner_partial.mustache1
-rw-r--r--pystache/tests/examples/inner_partial.txt1
-rw-r--r--pystache/tests/examples/inverted.mustache1
-rw-r--r--pystache/tests/examples/inverted.py33
-rw-r--r--pystache/tests/examples/lambdas.mustache1
-rw-r--r--pystache/tests/examples/lambdas.py38
-rw-r--r--pystache/tests/examples/looping_partial.mustache1
-rw-r--r--pystache/tests/examples/nested_context.mustache1
-rw-r--r--pystache/tests/examples/nested_context.py32
-rw-r--r--pystache/tests/examples/partial_in_partial.mustache1
-rw-r--r--pystache/tests/examples/partial_with_lambda.mustache1
-rw-r--r--pystache/tests/examples/partial_with_partial_and_lambda.mustache1
-rw-r--r--pystache/tests/examples/partials_with_lambdas.py12
-rw-r--r--pystache/tests/examples/readme.py9
-rw-r--r--pystache/tests/examples/say_hello.mustache1
-rw-r--r--pystache/tests/examples/simple.mustache1
-rw-r--r--pystache/tests/examples/simple.py15
-rw-r--r--pystache/tests/examples/tagless.mustache1
-rw-r--r--pystache/tests/examples/template_partial.mustache2
-rw-r--r--pystache/tests/examples/template_partial.py27
-rw-r--r--pystache/tests/examples/template_partial.txt4
-rw-r--r--pystache/tests/examples/unescaped.mustache1
-rw-r--r--pystache/tests/examples/unescaped.py10
-rw-r--r--pystache/tests/examples/unicode_input.mustache1
-rw-r--r--pystache/tests/examples/unicode_input.py14
-rw-r--r--pystache/tests/examples/unicode_output.mustache1
-rw-r--r--pystache/tests/examples/unicode_output.py11
-rw-r--r--pystache/tests/main.py155
-rw-r--r--pystache/tests/spectesting.py285
-rw-r--r--pystache/tests/test___init__.py36
-rw-r--r--pystache/tests/test_commands.py45
-rw-r--r--pystache/tests/test_context.py444
-rw-r--r--pystache/tests/test_examples.py106
-rw-r--r--pystache/tests/test_loader.py194
-rw-r--r--pystache/tests/test_locator.py157
-rw-r--r--pystache/tests/test_parser.py26
-rw-r--r--pystache/tests/test_pystache.py127
-rw-r--r--pystache/tests/test_renderengine.py610
-rw-r--r--pystache/tests/test_renderer.py584
-rw-r--r--pystache/tests/test_simple.py82
-rw-r--r--pystache/tests/test_specloader.py423
81 files changed, 4286 insertions, 94 deletions
diff --git a/pystache/__init__.py b/pystache/__init__.py
index daf7f52..5f5035d 100644
--- a/pystache/__init__.py
+++ b/pystache/__init__.py
@@ -1,2 +1,13 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
# We keep all initialization code in a separate module.
-from init import *
+
+from pystache.init import render, Renderer, TemplateSpec
+
+__all__ = ['render', 'Renderer', 'TemplateSpec']
+
+__version__ = '0.5.1' # Also change in setup.py.
diff --git a/pystache/commands/__init__.py b/pystache/commands/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/commands/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/commands.py b/pystache/commands/render.py
index 1801d40..23b19f8 100644
--- a/pystache/commands.py
+++ b/pystache/commands/render.py
@@ -13,7 +13,16 @@ try:
except:
# The json module is new in Python 2.6, whereas simplejson is
# compatible with earlier versions.
- import simplejson as json
+ try:
+ import simplejson as json
+ except ImportError:
+ # Raise an error with a type different from ImportError as a hack around
+ # this issue:
+ # http://bugs.python.org/issue7559
+ from sys import exc_info
+ ex_type, ex_value, tb = exc_info()
+ new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
+ raise new_ex.__class__, new_ex, tb
# The optparse module is deprecated in Python 2.7 in favor of argparse.
# However, argparse is not available in Python 2.6 and earlier.
@@ -54,7 +63,12 @@ def parse_args(sys_argv, usage):
return template, context
-def main(sys_argv):
+# TODO: verify whether the setup() method's entry_points argument
+# supports passing arguments to main:
+#
+# http://packages.python.org/distribute/setuptools.html#automatic-script-creation
+#
+def main(sys_argv=sys.argv):
template, context = parse_args(sys_argv, USAGE)
if template.endswith('.mustache'):
@@ -77,5 +91,4 @@ def main(sys_argv):
if __name__=='__main__':
- main(sys.argv)
-
+ main()
diff --git a/pystache/commands/test.py b/pystache/commands/test.py
new file mode 100644
index 0000000..0872453
--- /dev/null
+++ b/pystache/commands/test.py
@@ -0,0 +1,18 @@
+# coding: utf-8
+
+"""
+This module provides a command to test pystache (unit tests, doctests, etc).
+
+"""
+
+import sys
+
+from pystache.tests.main import main as run_tests
+
+
+def main(sys_argv=sys.argv):
+ run_tests(sys_argv=sys_argv)
+
+
+if __name__=='__main__':
+ main()
diff --git a/pystache/common.py b/pystache/common.py
new file mode 100644
index 0000000..00f8a77
--- /dev/null
+++ b/pystache/common.py
@@ -0,0 +1,26 @@
+# coding: utf-8
+
+"""
+Exposes common functions.
+
+"""
+
+# This function was designed to be portable across Python versions -- both
+# with older versions and with Python 3 after applying 2to3.
+def read(path):
+ """
+ Return the contents of a text file as a byte string.
+
+ """
+ # Opening in binary mode is necessary for compatibility across Python
+ # 2 and 3. In both Python 2 and 3, open() defaults to opening files in
+ # text mode. However, in Python 2, open() returns file objects whose
+ # read() method returns byte strings (strings of type `str` in Python 2),
+ # whereas in Python 3, the file object returns unicode strings (strings
+ # of type `str` in Python 3).
+ f = open(path, 'rb')
+ # We avoid use of the with keyword for Python 2.4 support.
+ try:
+ return f.read()
+ finally:
+ f.close()
diff --git a/pystache/context.py b/pystache/context.py
index d5570da..d0cba5d 100644
--- a/pystache/context.py
+++ b/pystache/context.py
@@ -1,15 +1,24 @@
# coding: utf-8
"""
-Defines a Context class to represent mustache(5)'s notion of context.
+Exposes a ContextStack class and functions to retrieve names from context.
"""
-class NotFound(object): pass
+# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
+_BUILTIN_MODULE = type(0).__module__
+
+
# We use this private global variable as a return value to represent a key
# not being found on lookup. This lets us distinguish between the case
# of a key's value being None with the case of a key not being found --
# without having to rely on exceptions (e.g. KeyError) for flow control.
+#
+# TODO: eliminate the need for a private global variable, e.g. by using the
+# preferred Python approach of "easier to ask for forgiveness than permission":
+# http://docs.python.org/glossary.html#term-eafp
+class NotFound(object):
+ pass
_NOT_FOUND = NotFound()
@@ -24,7 +33,7 @@ def _get_value(item, key):
Returns _NOT_FOUND if the key does not exist.
- The Context.get() docstring documents this function's intended behavior.
+ The ContextStack.get() docstring documents this function's intended behavior.
"""
parts = key.split('.')
@@ -39,7 +48,7 @@ def _get_value(item, key):
# (e.g. catching KeyError).
if key in item:
value = item[key]
- elif type(item).__module__ != '__builtin__':
+ elif type(item).__module__ != _BUILTIN_MODULE:
# Then we consider the argument an "object" for the purposes of
# the spec.
#
@@ -60,7 +69,26 @@ def _get_value(item, key):
return value
-class Context(object):
+# TODO: add some unit tests for this.
+def resolve(context, name):
+ """
+ Resolve the given name against the given context stack.
+
+ This function follows the rules outlined in the section of the spec
+ regarding tag interpolation.
+
+ This function does not coerce the return value to a string.
+
+ """
+ if name == '.':
+ return context.top()
+
+ # The spec says that if the name fails resolution, the result should be
+ # considered falsey, and should interpolate as the empty string.
+ return context.get(name, '')
+
+
+class ContextStack(object):
"""
Provides dictionary-like access to a stack of zero or more items.
@@ -75,7 +103,7 @@ class Context(object):
(last in, first out).
Caution: this class does not currently support recursive nesting in
- that items in the stack cannot themselves be Context instances.
+ that items in the stack cannot themselves be ContextStack instances.
See the docstrings of the methods of this class for more details.
@@ -92,7 +120,7 @@ class Context(object):
stack in order so that, in particular, items at the end of
the argument list are queried first when querying the stack.
- Caution: items should not themselves be Context instances, as
+ Caution: items should not themselves be ContextStack instances, as
recursive nesting does not behave as one might expect.
"""
@@ -104,9 +132,9 @@ class Context(object):
For example--
- >>> context = Context({'alpha': 'abc'}, {'numeric': 123})
+ >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
>>> repr(context)
- "Context({'alpha': 'abc'}, {'numeric': 123})"
+ "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
"""
return "%s%s" % (self.__class__.__name__, tuple(self._stack))
@@ -114,18 +142,18 @@ class Context(object):
@staticmethod
def create(*context, **kwargs):
"""
- Build a Context instance from a sequence of context-like items.
+ Build a ContextStack instance from a sequence of context-like items.
- This factory-style method is more general than the Context class's
+ This factory-style method is more general than the ContextStack class's
constructor in that, unlike the constructor, the argument list
- can itself contain Context instances.
+ can itself contain ContextStack instances.
Here is an example illustrating various aspects of this method:
>>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
- >>> obj2 = Context({'vegetable': 'spinach', 'mineral': 'silver'})
+ >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
>>>
- >>> context = Context.create(obj1, None, obj2, mineral='gold')
+ >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
>>>
>>> context.get('animal')
'cat'
@@ -136,7 +164,7 @@ class Context(object):
Arguments:
- *context: zero or more dictionaries, Context instances, or objects
+ *context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
arguments will be skipped. Items in the *context list are
added to the stack in order so that later items in the argument
@@ -152,12 +180,12 @@ class Context(object):
"""
items = context
- context = Context()
+ context = ContextStack()
for item in items:
if item is None:
continue
- if isinstance(item, Context):
+ if isinstance(item, ContextStack):
context._stack.extend(item._stack)
else:
context.push(item)
@@ -226,9 +254,9 @@ class Context(object):
>>>
>>> dct['greet'] is obj.greet
True
- >>> Context(dct).get('greet') #doctest: +ELLIPSIS
+ >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
<function greet at 0x...>
- >>> Context(obj).get('greet')
+ >>> ContextStack(obj).get('greet')
'Hi Bob!'
TODO: explain the rationale for this difference in treatment.
@@ -270,4 +298,4 @@ class Context(object):
Return a copy of this instance.
"""
- return Context(*self._stack)
+ return ContextStack(*self._stack)
diff --git a/pystache/defaults.py b/pystache/defaults.py
index b696410..fcd04c3 100644
--- a/pystache/defaults.py
+++ b/pystache/defaults.py
@@ -8,7 +8,12 @@ does not otherwise specify a value.
"""
-import cgi
+try:
+ # Python 3.2 adds html.escape() and deprecates cgi.escape().
+ from html import escape
+except ImportError:
+ from cgi import escape
+
import os
import sys
@@ -39,12 +44,14 @@ SEARCH_DIRS = [os.curdir] # i.e. ['.']
# rendering templates (e.g. for tags enclosed in double braces).
# Only unicode strings will be passed to this function.
#
-# The quote=True argument causes double quotes to be escaped,
-# but not single quotes:
+# The quote=True argument causes double but not single quotes to be escaped
+# in Python 3.1 and earlier, and both double and single quotes to be
+# escaped in Python 3.2 and later:
#
# http://docs.python.org/library/cgi.html#cgi.escape
+# http://docs.python.org/dev/library/html.html#html.escape
#
-TAG_ESCAPE = lambda u: cgi.escape(u, quote=True)
+TAG_ESCAPE = lambda u: escape(u, quote=True)
# The default template extension.
TEMPLATE_EXTENSION = 'mustache'
diff --git a/pystache/init.py b/pystache/init.py
index b285a5c..e9d854d 100644
--- a/pystache/init.py
+++ b/pystache/init.py
@@ -9,9 +9,6 @@ from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
-__all__ = ['render', 'Renderer', 'TemplateSpec']
-
-
def render(template, context=None, **kwargs):
"""
Return the given template string rendered using the given context.
diff --git a/pystache/loader.py b/pystache/loader.py
index bcba71b..0fdadc5 100644
--- a/pystache/loader.py
+++ b/pystache/loader.py
@@ -8,18 +8,24 @@ This module provides a Loader class for locating and reading templates.
import os
import sys
+from pystache import common
from pystache import defaults
from pystache.locator import Locator
-def _to_unicode(s, encoding=None):
- """
- Raises a TypeError exception if the given string is already unicode.
+# We make a function so that the current defaults take effect.
+# TODO: revisit whether this is necessary.
- """
- if encoding is None:
- encoding = defaults.STRING_ENCODING
- return unicode(s, encoding, defaults.DECODE_ERRORS)
+def _make_to_unicode():
+ def to_unicode(s, encoding=None):
+ """
+ Raises a TypeError exception if the given string is already unicode.
+
+ """
+ if encoding is None:
+ encoding = defaults.STRING_ENCODING
+ return unicode(s, encoding, defaults.DECODE_ERRORS)
+ return to_unicode
class Loader(object):
@@ -67,7 +73,7 @@ class Loader(object):
search_dirs = defaults.SEARCH_DIRS
if to_unicode is None:
- to_unicode = _to_unicode
+ to_unicode = _make_to_unicode()
self.extension = extension
self.file_encoding = file_encoding
@@ -106,17 +112,12 @@ class Loader(object):
Read the template at the given path, and return it as a unicode string.
"""
- # We avoid use of the with keyword for Python 2.4 support.
- f = open(path, 'r')
- try:
- text = f.read()
- finally:
- f.close()
+ b = common.read(path)
if encoding is None:
encoding = self.file_encoding
- return self.unicode(text, encoding)
+ return self.unicode(b, encoding)
# TODO: unit-test this method.
def load_name(self, name):
diff --git a/pystache/parsed.py b/pystache/parsed.py
index 5418ec1..552af55 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -17,7 +17,7 @@ class ParsedTemplate(object):
parse_tree: a list, each element of which is either--
(1) a unicode string, or
- (2) a "rendering" callable that accepts a Context instance
+ (2) a "rendering" callable that accepts a ContextStack instance
and returns a unicode string.
The possible rendering callables are the return values of the
diff --git a/pystache/parser.py b/pystache/parser.py
index d07ebf6..2b97405 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -9,7 +9,7 @@ This module is only meant for internal use by the renderengine module.
import re
-from parsed import ParsedTemplate
+from pystache.parsed import ParsedTemplate
DEFAULT_DELIMITERS = ('{{', '}}')
@@ -17,7 +17,13 @@ END_OF_LINE_CHARACTERS = ['\r', '\n']
NON_BLANK_RE = re.compile(r'^(.)', re.M)
-def _compile_template_re(delimiters):
+def _compile_template_re(delimiters=None):
+ """
+ Return a regular expresssion object (re.RegexObject) instance.
+
+ """
+ if delimiters is None:
+ delimiters = DEFAULT_DELIMITERS
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
@@ -74,19 +80,25 @@ class Parser(object):
self._delimiters = delimiters
self.compile_template_re()
- def parse(self, template, index=0, section_key=None):
+ def parse(self, template, start_index=0, section_key=None):
"""
- Parse a template string into a ParsedTemplate instance.
+ Parse a template string starting at some index.
This method uses the current tag delimiter.
Arguments:
- template: a template string of type unicode.
+ template: a unicode string that is the template to parse.
+
+ index: the index at which to start parsing.
+
+ Returns:
+
+ a ParsedTemplate instance.
"""
parse_tree = []
- start_index = index
+ index = start_index
while True:
match = self._template_re.search(template, index)
@@ -131,9 +143,9 @@ class Parser(object):
if tag_type == '/':
if tag_key != section_key:
- raise ParsingError("Section end tag mismatch: %s != %s" % (repr(tag_key), repr(section_key)))
+ raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
- return ParsedTemplate(parse_tree), template[start_index:match_index], end_index
+ return ParsedTemplate(parse_tree), match_index, end_index
index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
@@ -142,10 +154,33 @@ class Parser(object):
return ParsedTemplate(parse_tree)
- def _parse_section(self, template, index_start, section_key):
- parsed_template, template, index_end = self.parse(template=template, index=index_start, section_key=section_key)
+ def _parse_section(self, template, start_index, section_key):
+ """
+ Parse the contents of a template section.
+
+ Arguments:
+
+ template: a unicode template string.
+
+ start_index: the string index at which the section contents begin.
+
+ section_key: the tag key of the section.
+
+ Returns: a 3-tuple:
+
+ parsed_section: the section contents parsed as a ParsedTemplate
+ instance.
+
+ content_end_index: the string index after the section contents.
+
+ end_index: the string index after the closing section tag (and
+ including any trailing newlines).
+
+ """
+ parsed_section, content_end_index, end_index = \
+ self.parse(template=template, start_index=start_index, section_key=section_key)
- return parsed_template, template, index_end
+ return parsed_section, template[start_index:content_end_index], end_index
def _handle_tag_type(self, template, parse_tree, tag_type, tag_key, leading_whitespace, end_index):
@@ -170,12 +205,12 @@ class Parser(object):
elif tag_type == '#':
- parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_section(tag_key, parsed_section, template, self._delimiters)
+ parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
+ func = engine._make_get_section(tag_key, parsed_section, section_contents, self._delimiters)
elif tag_type == '^':
- parsed_section, template, end_index = self._parse_section(template, end_index, tag_key)
+ parsed_section, section_contents, end_index = self._parse_section(template, end_index, tag_key)
func = engine._make_get_inverse(tag_key, parsed_section)
elif tag_type == '>':
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index 4361dca..9e4da11 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -7,7 +7,8 @@ Defines a class responsible for rendering logic.
import re
-from parser import Parser
+from pystache.context import resolve
+from pystache.parser import Parser
class RenderEngine(object):
@@ -55,7 +56,7 @@ class RenderEngine(object):
this class will not pass tag values to literal prior to passing
them to this function. This allows for more flexibility,
for example using a custom escape function that handles
- incoming strings of type markupssafe.Markup differently
+ incoming strings of type markupsafe.Markup differently
from plain unicode strings.
"""
@@ -68,16 +69,7 @@ class RenderEngine(object):
Get a value from the given context as a basestring instance.
"""
- val = context.get(tag_name)
-
- # We use "==" rather than "is" to compare integers, as using "is"
- # relies on an implementation detail of CPython. The test about
- # rendering zeroes failed while using PyPy when using "is".
- # See issue #34: https://github.com/defunkt/pystache/issues/34
- if not val and val != 0:
- if tag_name != '.':
- return ''
- val = context.top()
+ val = resolve(context, tag_name)
if callable(val):
# According to the spec:
@@ -142,6 +134,8 @@ class RenderEngine(object):
Returns a string with type unicode.
"""
+ # TODO: is there a bug because we are not using the same
+ # logic as in _get_string_value()?
data = context.get(name)
if data:
return u''
@@ -167,9 +161,33 @@ class RenderEngine(object):
# TODO: should we check the arity?
template = data(template)
parsed_template = self._parse(template, delimiters=delims)
- data = [ data ]
- elif not hasattr(data, '__iter__') or isinstance(data, dict):
- data = [ data ]
+ # Lambdas special case section rendering and bypass pushing
+ # the data value onto the context stack. Also see--
+ #
+ # https://github.com/defunkt/pystache/issues/113
+ #
+ return parsed_template.render(context)
+ else:
+ # The cleanest, least brittle way of determining whether
+ # something supports iteration is by trying to call iter() on it:
+ #
+ # http://docs.python.org/library/functions.html#iter
+ #
+ # It is not sufficient, for example, to check whether the item
+ # implements __iter__ () (the iteration protocol). There is
+ # also __getitem__() (the sequence protocol). In Python 2,
+ # strings do not implement __iter__(), but in Python 3 they do.
+ try:
+ iter(data)
+ except TypeError:
+ # Then the value does not support iteration.
+ data = [data]
+ else:
+ # We treat the value as a list (but do not treat strings
+ # and dicts as lists).
+ if isinstance(data, (basestring, dict)):
+ data = [data]
+ # Otherwise, leave it alone.
parts = []
for element in data:
@@ -202,7 +220,7 @@ class RenderEngine(object):
Arguments:
template: a template string of type unicode.
- context: a Context instance.
+ context: a ContextStack instance.
"""
# We keep this type-check as an added check because this method is
@@ -225,7 +243,7 @@ class RenderEngine(object):
template: a template string of type unicode (but not a proper
subclass of unicode).
- context: a Context instance.
+ context: a ContextStack instance.
"""
# Be strict but not too strict. In other words, accept str instead
diff --git a/pystache/renderer.py b/pystache/renderer.py
index 5bd2a3f..26f271f 100644
--- a/pystache/renderer.py
+++ b/pystache/renderer.py
@@ -5,14 +5,27 @@ This module provides a Renderer class to render templates.
"""
+import sys
+
from pystache import defaults
-from pystache.context import Context
+from pystache.context import ContextStack
from pystache.loader import Loader
from pystache.renderengine import RenderEngine
-from pystache.spec_loader import SpecLoader
+from pystache.specloader import SpecLoader
from pystache.template_spec import TemplateSpec
+# TODO: come up with a better solution for this. One of the issues here
+# is that in Python 3 there is no common base class for unicode strings
+# and byte strings, and 2to3 seems to convert all of "str", "unicode",
+# and "basestring" to Python 3's "str".
+if sys.version_info < (3, ):
+ _STRING_TYPES = basestring
+else:
+ # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
+ _STRING_TYPES = (unicode, type(u"a".encode('utf-8')))
+
+
class Renderer(object):
"""
@@ -27,8 +40,9 @@ class Renderer(object):
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
- >>> renderer.render('{{>partial}}', {'thing': 'world'})
- u'Hello, world!'
+ >>> # We apply print to make the test work in Python 3 after 2to3.
+ >>> print renderer.render('{{>partial}}', {'thing': 'world'})
+ Hello, world!
"""
@@ -64,10 +78,10 @@ class Renderer(object):
this class will only pass it unicode strings. The constructor
assigns this function to the constructed instance's escape()
method.
- The argument defaults to `cgi.escape(s, quote=True)`. To
- disable escaping entirely, one can pass `lambda u: u` as the
- escape function, for example. One may also wish to consider
- using markupsafe's escape function: markupsafe.escape().
+ To disable escaping entirely, one can pass `lambda u: u`
+ as the escape function, for example. One may also wish to
+ consider using markupsafe's escape function: markupsafe.escape().
+ This argument defaults to the package default.
file_encoding: the name of the default encoding to use when reading
template files. All templates are converted to unicode prior
@@ -160,9 +174,16 @@ class Renderer(object):
"""
return unicode(self.escape(self._to_unicode_soft(s)))
- def unicode(self, s, encoding=None):
+ def unicode(self, b, encoding=None):
"""
- Convert a string to unicode, using string_encoding and decode_errors.
+ Convert a byte string to unicode, using string_encoding and decode_errors.
+
+ Arguments:
+
+ b: a byte string.
+
+ encoding: the name of an encoding. Defaults to the string_encoding
+ attribute for this instance.
Raises:
@@ -178,7 +199,7 @@ class Renderer(object):
# TODO: Wrap UnicodeDecodeErrors with a message about setting
# the string_encoding and decode_errors attributes.
- return unicode(s, encoding, self.decode_errors)
+ return unicode(b, encoding, self.decode_errors)
def _make_loader(self):
"""
@@ -256,7 +277,7 @@ class Renderer(object):
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
- context = Context.create(*context, **kwargs)
+ context = ContextStack.create(*context, **kwargs)
self._context = context
engine = self._make_render_engine()
@@ -317,7 +338,7 @@ class Renderer(object):
uses the passed object as the first element of the context stack
when rendering.
- *context: zero or more dictionaries, Context instances, or objects
+ *context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
arguments are skipped. Items in the *context list are added to
the context stack in order so that later items in the argument
@@ -329,7 +350,7 @@ class Renderer(object):
all items in the *context list.
"""
- if isinstance(template, basestring):
+ if isinstance(template, _STRING_TYPES):
return self._render_string(template, *context, **kwargs)
# Otherwise, we assume the template is an object.
diff --git a/pystache/spec_loader.py b/pystache/specloader.py
index 3cb0f1a..3cb0f1a 100644
--- a/pystache/spec_loader.py
+++ b/pystache/specloader.py
diff --git a/pystache/template_spec.py b/pystache/template_spec.py
index c33f30b..76ce784 100644
--- a/pystache/template_spec.py
+++ b/pystache/template_spec.py
@@ -1,7 +1,11 @@
# coding: utf-8
"""
-This module supports customized (aka special or specified) template loading.
+Provides a class to customize template information on a per-view basis.
+
+To customize template properties for a particular view, create that view
+from a class that subclasses TemplateSpec. The "Spec" in TemplateSpec
+stands for template information that is "special" or "specified".
"""
diff --git a/pystache/tests/__init__.py b/pystache/tests/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/tests/benchmark.py b/pystache/tests/benchmark.py
new file mode 100755
index 0000000..d46e973
--- /dev/null
+++ b/pystache/tests/benchmark.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+"""
+A rudimentary backward- and forward-compatible script to benchmark pystache.
+
+Usage:
+
+tests/benchmark.py 10000
+
+"""
+
+import sys
+from timeit import Timer
+
+import pystache
+
+# TODO: make the example realistic.
+
+examples = [
+ # Test case: 1
+ ("""{{#person}}Hi {{name}}{{/person}}""",
+ {"person": {"name": "Jon"}},
+ "Hi Jon"),
+
+ # Test case: 2
+ ("""\
+<div class="comments">
+<h3>{{header}}</h3>
+<ul>
+{{#comments}}<li class="comment">
+<h5>{{name}}</h5><p>{{body}}</p>
+</li>{{/comments}}
+</ul>
+</div>""",
+ {'header': "My Post Comments",
+ 'comments': [
+ {'name': "Joe", 'body': "Thanks for this post!"},
+ {'name': "Sam", 'body': "Thanks for this post!"},
+ {'name': "Heather", 'body': "Thanks for this post!"},
+ {'name': "Kathy", 'body': "Thanks for this post!"},
+ {'name': "George", 'body': "Thanks for this post!"}]},
+ """\
+<div class="comments">
+<h3>My Post Comments</h3>
+<ul>
+<li class="comment">
+<h5>Joe</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Sam</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Heather</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>Kathy</h5><p>Thanks for this post!</p>
+</li><li class="comment">
+<h5>George</h5><p>Thanks for this post!</p>
+</li>
+</ul>
+</div>"""),
+]
+
+
+def make_test_function(example):
+
+ template, context, expected = example
+
+ def test():
+ actual = pystache.render(template, context)
+ if actual != expected:
+ raise Exception("Benchmark mismatch: \n%s\n*** != ***\n%s" % (expected, actual))
+
+ return test
+
+
+def main(sys_argv):
+ args = sys_argv[1:]
+ count = int(args[0])
+
+ print "Benchmarking: %sx" % count
+ print
+
+ for example in examples:
+
+ test = make_test_function(example)
+
+ t = Timer(test,)
+ print min(t.repeat(repeat=3, number=count))
+
+ print "Done"
+
+
+if __name__ == '__main__':
+ main(sys.argv)
+
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
new file mode 100644
index 0000000..a99e709
--- /dev/null
+++ b/pystache/tests/common.py
@@ -0,0 +1,193 @@
+# coding: utf-8
+
+"""
+Provides test-related code that can be used by all tests.
+
+"""
+
+import os
+
+import pystache
+from pystache import defaults
+from pystache.tests import examples
+
+# Save a reference to the original function to avoid recursion.
+_DEFAULT_TAG_ESCAPE = defaults.TAG_ESCAPE
+_TESTS_DIR = os.path.dirname(pystache.tests.__file__)
+
+DATA_DIR = os.path.join(_TESTS_DIR, 'data') # i.e. 'pystache/tests/data'.
+EXAMPLES_DIR = os.path.dirname(examples.__file__)
+PACKAGE_DIR = os.path.dirname(pystache.__file__)
+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']
+
+UNITTEST_FILE_PREFIX = "test_"
+
+
+def html_escape(u):
+ """
+ An html escape function that behaves the same in both Python 2 and 3.
+
+ This function is needed because single quotes are escaped in Python 3
+ (to '&#x27;'), but not in Python 2.
+
+ The global defaults.TAG_ESCAPE can be set to this function in the
+ setUp() and tearDown() of unittest test cases, for example, for
+ consistent test results.
+
+ """
+ u = _DEFAULT_TAG_ESCAPE(u)
+ return u.replace("'", '&#x27;')
+
+
+def get_data_path(file_name):
+ return os.path.join(DATA_DIR, file_name)
+
+
+# Functions related to get_module_names().
+
+def _find_files(root_dir, should_include):
+ """
+ Return a list of paths to all modules below the given directory.
+
+ Arguments:
+
+ should_include: a function that accepts a file path and returns True or False.
+
+ """
+ paths = [] # Return value.
+
+ is_module = lambda path: path.endswith(".py")
+
+ # os.walk() is new in Python 2.3
+ # http://docs.python.org/library/os.html#os.walk
+ for dir_path, dir_names, file_names in os.walk(root_dir):
+ new_paths = [os.path.join(dir_path, file_name) for file_name in file_names]
+ new_paths = filter(is_module, new_paths)
+ new_paths = filter(should_include, new_paths)
+ paths.extend(new_paths)
+
+ return paths
+
+
+def _make_module_names(package_dir, paths):
+ """
+ Return a list of fully-qualified module names given a list of module paths.
+
+ """
+ package_dir = os.path.abspath(package_dir)
+ package_name = os.path.split(package_dir)[1]
+
+ prefix_length = len(package_dir)
+
+ module_names = []
+ for path in paths:
+ path = os.path.abspath(path) # for example <path_to_package>/subpackage/module.py
+ rel_path = path[prefix_length:] # for example /subpackage/module.py
+ rel_path = os.path.splitext(rel_path)[0] # for example /subpackage/module
+
+ parts = []
+ while True:
+ (rel_path, tail) = os.path.split(rel_path)
+ if not tail:
+ break
+ parts.insert(0, tail)
+ # We now have, for example, ['subpackage', 'module'].
+ parts.insert(0, package_name)
+ module = ".".join(parts)
+ module_names.append(module)
+
+ return module_names
+
+
+def get_module_names(package_dir=None, should_include=None):
+ """
+ Return a list of fully-qualified module names in the given package.
+
+ """
+ if package_dir is None:
+ package_dir = PACKAGE_DIR
+
+ if should_include is None:
+ should_include = lambda path: True
+
+ paths = _find_files(package_dir, should_include)
+ names = _make_module_names(package_dir, paths)
+ names.sort()
+
+ return names
+
+
+class AssertStringMixin:
+
+ """A unittest.TestCase mixin to check string equality."""
+
+ def assertString(self, actual, expected, format=None):
+ """
+ Assert that the given strings are equal and have the same type.
+
+ Arguments:
+
+ format: a format string containing a single conversion specifier %s.
+ Defaults to "%s".
+
+ """
+ if format is None:
+ format = "%s"
+
+ # Show both friendly and literal versions.
+ details = """String mismatch: %%s\
+
+
+ Expected: \"""%s\"""
+ Actual: \"""%s\"""
+
+ Expected: %s
+ Actual: %s""" % (expected, actual, repr(expected), repr(actual))
+
+ def make_message(reason):
+ description = details % reason
+ return format % description
+
+ self.assertEqual(actual, expected, make_message("different characters"))
+
+ reason = "types different: %s != %s (actual)" % (repr(type(expected)), repr(type(actual)))
+ self.assertEqual(type(expected), type(actual), make_message(reason))
+
+
+class AssertIsMixin:
+
+ """A unittest.TestCase mixin adding assertIs()."""
+
+ # unittest.assertIs() is not available until Python 2.7:
+ # http://docs.python.org/library/unittest.html#unittest.TestCase.assertIsNone
+ def assertIs(self, first, second):
+ self.assertTrue(first is second, msg="%s is not %s" % (repr(first), repr(second)))
+
+
+class SetupDefaults(object):
+
+ """
+ Mix this class in to a unittest.TestCase for standard defaults.
+
+ This class allows for consistent test results across Python 2/3.
+
+ """
+
+ def setup_defaults(self):
+ self.original_decode_errors = defaults.DECODE_ERRORS
+ self.original_file_encoding = defaults.FILE_ENCODING
+ self.original_string_encoding = defaults.STRING_ENCODING
+
+ defaults.DECODE_ERRORS = 'strict'
+ defaults.FILE_ENCODING = 'ascii'
+ defaults.STRING_ENCODING = 'ascii'
+
+ def teardown_defaults(self):
+ defaults.DECODE_ERRORS = self.original_decode_errors
+ defaults.FILE_ENCODING = self.original_file_encoding
+ defaults.STRING_ENCODING = self.original_string_encoding
+
diff --git a/pystache/tests/data/__init__.py b/pystache/tests/data/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/data/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/tests/data/ascii.mustache b/pystache/tests/data/ascii.mustache
new file mode 100644
index 0000000..e86737b
--- /dev/null
+++ b/pystache/tests/data/ascii.mustache
@@ -0,0 +1 @@
+ascii: abc \ No newline at end of file
diff --git a/pystache/tests/data/duplicate.mustache b/pystache/tests/data/duplicate.mustache
new file mode 100644
index 0000000..a0515e3
--- /dev/null
+++ b/pystache/tests/data/duplicate.mustache
@@ -0,0 +1 @@
+This file is used to test locate_path()'s search order. \ No newline at end of file
diff --git a/pystache/tests/data/locator/__init__.py b/pystache/tests/data/locator/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/data/locator/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/tests/data/locator/duplicate.mustache b/pystache/tests/data/locator/duplicate.mustache
new file mode 100644
index 0000000..a0515e3
--- /dev/null
+++ b/pystache/tests/data/locator/duplicate.mustache
@@ -0,0 +1 @@
+This file is used to test locate_path()'s search order. \ No newline at end of file
diff --git a/pystache/tests/data/non_ascii.mustache b/pystache/tests/data/non_ascii.mustache
new file mode 100644
index 0000000..bd69b61
--- /dev/null
+++ b/pystache/tests/data/non_ascii.mustache
@@ -0,0 +1 @@
+non-ascii: é \ No newline at end of file
diff --git a/pystache/tests/data/sample_view.mustache b/pystache/tests/data/sample_view.mustache
new file mode 100644
index 0000000..e86737b
--- /dev/null
+++ b/pystache/tests/data/sample_view.mustache
@@ -0,0 +1 @@
+ascii: abc \ No newline at end of file
diff --git a/pystache/tests/data/say_hello.mustache b/pystache/tests/data/say_hello.mustache
new file mode 100644
index 0000000..84ab4c9
--- /dev/null
+++ b/pystache/tests/data/say_hello.mustache
@@ -0,0 +1 @@
+Hello, {{to}} \ No newline at end of file
diff --git a/pystache/tests/data/views.py b/pystache/tests/data/views.py
new file mode 100644
index 0000000..0b96309
--- /dev/null
+++ b/pystache/tests/data/views.py
@@ -0,0 +1,21 @@
+# coding: utf-8
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class SayHello(object):
+
+ def to(self):
+ return "World"
+
+
+class SampleView(TemplateSpec):
+ pass
+
+
+class NonAscii(TemplateSpec):
+ pass
diff --git a/pystache/tests/doctesting.py b/pystache/tests/doctesting.py
new file mode 100644
index 0000000..469c81e
--- /dev/null
+++ b/pystache/tests/doctesting.py
@@ -0,0 +1,90 @@
+# coding: utf-8
+
+"""
+Exposes a get_doctests() function for the project's test harness.
+
+"""
+
+import doctest
+import os
+import pkgutil
+import sys
+import traceback
+
+if sys.version_info >= (3,):
+ # Then pull in modules needed for 2to3 conversion. The modules
+ # below are not necessarily available in older versions of Python.
+ from lib2to3.main import main as lib2to3main # new in Python 2.6?
+ from shutil import copyfile
+
+from pystache.tests.common import TEXT_DOCTEST_PATHS
+from pystache.tests.common import get_module_names
+
+
+# This module follows the guidance documented here:
+#
+# http://docs.python.org/library/doctest.html#unittest-api
+#
+
+def get_doctests(text_file_dir):
+ """
+ Return a list of TestSuite instances for all doctests in the project.
+
+ Arguments:
+
+ text_file_dir: the directory in which to search for all text files
+ (i.e. non-module files) containing doctests.
+
+ """
+ # Since module_relative is False in our calls to DocFileSuite below,
+ # paths should be OS-specific. See the following for more info--
+ #
+ # http://docs.python.org/library/doctest.html#doctest.DocFileSuite
+ #
+ 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)
+
+ suites = []
+
+ for path in paths:
+ suite = doctest.DocFileSuite(path, module_relative=False)
+ suites.append(suite)
+
+ modules = get_module_names()
+ for module in modules:
+ suite = doctest.DocTestSuite(module)
+ suites.append(suite)
+
+ return suites
+
+
+def _convert_2to3(path):
+ """
+ Convert the given file, and return the path to the converted files.
+
+ """
+ base, ext = os.path.splitext(path)
+ # For example, "README.temp2to3.rst".
+ new_path = "%s.temp2to3%s" % (base, ext)
+
+ copyfile(path, new_path)
+
+ args = ['--doctests_only', '--no-diffs', '--write', '--nobackups', new_path]
+ lib2to3main("lib2to3.fixes", args=args)
+
+ return new_path
+
+
+def _convert_paths(paths):
+ """
+ Convert the given files, and return the paths to the converted files.
+
+ """
+ new_paths = []
+ for path in paths:
+ new_path = _convert_2to3(path)
+ new_paths.append(new_path)
+
+ return new_paths
diff --git a/pystache/tests/examples/__init__.py b/pystache/tests/examples/__init__.py
new file mode 100644
index 0000000..a0d386a
--- /dev/null
+++ b/pystache/tests/examples/__init__.py
@@ -0,0 +1,4 @@
+"""
+TODO: add a docstring.
+
+"""
diff --git a/pystache/tests/examples/comments.mustache b/pystache/tests/examples/comments.mustache
new file mode 100644
index 0000000..2a2a08b
--- /dev/null
+++ b/pystache/tests/examples/comments.mustache
@@ -0,0 +1 @@
+<h1>{{title}}{{! just something interesting... #or not... }}</h1> \ No newline at end of file
diff --git a/pystache/tests/examples/comments.py b/pystache/tests/examples/comments.py
new file mode 100644
index 0000000..8d75f88
--- /dev/null
+++ b/pystache/tests/examples/comments.py
@@ -0,0 +1,10 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class Comments(object):
+
+ def title(self):
+ return "A Comedy of Errors"
diff --git a/pystache/tests/examples/complex.mustache b/pystache/tests/examples/complex.mustache
new file mode 100644
index 0000000..6de758b
--- /dev/null
+++ b/pystache/tests/examples/complex.mustache
@@ -0,0 +1,6 @@
+<h1>{{ header }}</h1>
+{{#list}}
+<ul>
+{{#item}}{{# current }}<li><strong>{{name}}</strong></li>
+{{/ current }}{{#link}}<li><a href="{{url}}">{{name}}</a></li>
+{{/link}}{{/item}}</ul>{{/list}}{{#empty}}<p>The list is empty.</p>{{/empty}} \ No newline at end of file
diff --git a/pystache/tests/examples/complex.py b/pystache/tests/examples/complex.py
new file mode 100644
index 0000000..c653db0
--- /dev/null
+++ b/pystache/tests/examples/complex.py
@@ -0,0 +1,26 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class Complex(object):
+
+ def header(self):
+ return "Colors"
+
+ def item(self):
+ items = []
+ items.append({ 'name': 'red', 'current': True, 'url': '#Red' })
+ items.append({ 'name': 'green', 'link': True, 'url': '#Green' })
+ items.append({ 'name': 'blue', 'link': True, 'url': '#Blue' })
+ return items
+
+ def list(self):
+ return not self.empty()
+
+ def empty(self):
+ return len(self.item()) == 0
+
+ def empty_list(self):
+ return [];
diff --git a/pystache/tests/examples/delimiters.mustache b/pystache/tests/examples/delimiters.mustache
new file mode 100644
index 0000000..92bea6d
--- /dev/null
+++ b/pystache/tests/examples/delimiters.mustache
@@ -0,0 +1,6 @@
+{{=<% %>=}}
+* <% first %>
+<%=| |=%>
+* | second |
+|={{ }}=|
+* {{ third }}
diff --git a/pystache/tests/examples/delimiters.py b/pystache/tests/examples/delimiters.py
new file mode 100644
index 0000000..a31ec1b
--- /dev/null
+++ b/pystache/tests/examples/delimiters.py
@@ -0,0 +1,16 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class Delimiters(object):
+
+ def first(self):
+ return "It worked the first time."
+
+ def second(self):
+ return "And it worked the second time."
+
+ def third(self):
+ return "Then, surprisingly, it worked the third time."
diff --git a/pystache/tests/examples/double_section.mustache b/pystache/tests/examples/double_section.mustache
new file mode 100644
index 0000000..61f1917
--- /dev/null
+++ b/pystache/tests/examples/double_section.mustache
@@ -0,0 +1,3 @@
+{{#t}}* first{{/t}}
+* {{two}}
+{{#t}}* third{{/t}} \ No newline at end of file
diff --git a/pystache/tests/examples/double_section.py b/pystache/tests/examples/double_section.py
new file mode 100644
index 0000000..c9736e4
--- /dev/null
+++ b/pystache/tests/examples/double_section.py
@@ -0,0 +1,13 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class DoubleSection(object):
+
+ def t(self):
+ return True
+
+ def two(self):
+ return "second"
diff --git a/pystache/tests/examples/escaped.mustache b/pystache/tests/examples/escaped.mustache
new file mode 100644
index 0000000..8be4ccb
--- /dev/null
+++ b/pystache/tests/examples/escaped.mustache
@@ -0,0 +1 @@
+<h1>{{title}}</h1> \ No newline at end of file
diff --git a/pystache/tests/examples/escaped.py b/pystache/tests/examples/escaped.py
new file mode 100644
index 0000000..5d72dde
--- /dev/null
+++ b/pystache/tests/examples/escaped.py
@@ -0,0 +1,10 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class Escaped(object):
+
+ def title(self):
+ return "Bear > Shark"
diff --git a/pystache/tests/examples/extensionless b/pystache/tests/examples/extensionless
new file mode 100644
index 0000000..452c9fe
--- /dev/null
+++ b/pystache/tests/examples/extensionless
@@ -0,0 +1 @@
+No file extension: {{foo}} \ No newline at end of file
diff --git a/pystache/tests/examples/inner_partial.mustache b/pystache/tests/examples/inner_partial.mustache
new file mode 100644
index 0000000..2863764
--- /dev/null
+++ b/pystache/tests/examples/inner_partial.mustache
@@ -0,0 +1 @@
+Again, {{title}}! \ No newline at end of file
diff --git a/pystache/tests/examples/inner_partial.txt b/pystache/tests/examples/inner_partial.txt
new file mode 100644
index 0000000..650c959
--- /dev/null
+++ b/pystache/tests/examples/inner_partial.txt
@@ -0,0 +1 @@
+## Again, {{title}}! ## \ No newline at end of file
diff --git a/pystache/tests/examples/inverted.mustache b/pystache/tests/examples/inverted.mustache
new file mode 100644
index 0000000..fbea98d
--- /dev/null
+++ b/pystache/tests/examples/inverted.mustache
@@ -0,0 +1 @@
+{{^f}}one{{/f}}, {{ two }}, {{^f}}three{{/f}}{{^t}}, four!{{/t}}{{^empty_list}}, empty list{{/empty_list}}{{^populated_list}}, shouldn't see me{{/populated_list}} \ No newline at end of file
diff --git a/pystache/tests/examples/inverted.py b/pystache/tests/examples/inverted.py
new file mode 100644
index 0000000..12212b4
--- /dev/null
+++ b/pystache/tests/examples/inverted.py
@@ -0,0 +1,33 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class Inverted(object):
+
+ def t(self):
+ return True
+
+ def f(self):
+ return False
+
+ def two(self):
+ return 'two'
+
+ def empty_list(self):
+ return []
+
+ def populated_list(self):
+ return ['some_value']
+
+class InvertedLists(Inverted, TemplateSpec):
+ template_name = 'inverted'
+
+ def t(self):
+ return [0, 1, 2]
+
+ def f(self):
+ return []
diff --git a/pystache/tests/examples/lambdas.mustache b/pystache/tests/examples/lambdas.mustache
new file mode 100644
index 0000000..9dffca5
--- /dev/null
+++ b/pystache/tests/examples/lambdas.mustache
@@ -0,0 +1 @@
+{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}} \ No newline at end of file
diff --git a/pystache/tests/examples/lambdas.py b/pystache/tests/examples/lambdas.py
new file mode 100644
index 0000000..3bc08ff
--- /dev/null
+++ b/pystache/tests/examples/lambdas.py
@@ -0,0 +1,38 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+def rot(s, n=13):
+ r = ""
+ for c in s:
+ cc = c
+ if cc.isalpha():
+ cc = cc.lower()
+ o = ord(cc)
+ ro = (o+n) % 122
+ if ro == 0: ro = 122
+ if ro < 97: ro += 96
+ cc = chr(ro)
+ r = ''.join((r,cc))
+ return r
+
+def replace(subject, this='foo', with_this='bar'):
+ return subject.replace(this, with_this)
+
+
+# This class subclasses TemplateSpec because at least one unit test
+# sets the template attribute.
+class Lambdas(TemplateSpec):
+
+ def replace_foo_with_bar(self, text=None):
+ return replace
+
+ def rot13(self, text=None):
+ return rot
+
+ def sort(self, text=None):
+ return lambda text: ''.join(sorted(text))
diff --git a/pystache/tests/examples/looping_partial.mustache b/pystache/tests/examples/looping_partial.mustache
new file mode 100644
index 0000000..577f736
--- /dev/null
+++ b/pystache/tests/examples/looping_partial.mustache
@@ -0,0 +1 @@
+Looping partial {{item}}! \ No newline at end of file
diff --git a/pystache/tests/examples/nested_context.mustache b/pystache/tests/examples/nested_context.mustache
new file mode 100644
index 0000000..ce570d6
--- /dev/null
+++ b/pystache/tests/examples/nested_context.mustache
@@ -0,0 +1 @@
+{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}} \ No newline at end of file
diff --git a/pystache/tests/examples/nested_context.py b/pystache/tests/examples/nested_context.py
new file mode 100644
index 0000000..a2661b9
--- /dev/null
+++ b/pystache/tests/examples/nested_context.py
@@ -0,0 +1,32 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class NestedContext(TemplateSpec):
+
+ def __init__(self, renderer):
+ self.renderer = renderer
+
+ def _context_get(self, key):
+ return self.renderer.context.get(key)
+
+ def outer_thing(self):
+ return "two"
+
+ def foo(self):
+ return {'thing1': 'one', 'thing2': 'foo'}
+
+ def derp(self):
+ return [{'inner': 'car'}]
+
+ def herp(self):
+ return [{'outer': 'car'}]
+
+ def nested_context_in_view(self):
+ if self._context_get('outer') == self._context_get('inner'):
+ return 'it works!'
+ return ''
diff --git a/pystache/tests/examples/partial_in_partial.mustache b/pystache/tests/examples/partial_in_partial.mustache
new file mode 100644
index 0000000..c61ceb1
--- /dev/null
+++ b/pystache/tests/examples/partial_in_partial.mustache
@@ -0,0 +1 @@
+{{>simple}} \ No newline at end of file
diff --git a/pystache/tests/examples/partial_with_lambda.mustache b/pystache/tests/examples/partial_with_lambda.mustache
new file mode 100644
index 0000000..2989f56
--- /dev/null
+++ b/pystache/tests/examples/partial_with_lambda.mustache
@@ -0,0 +1 @@
+{{#rot13}}abcdefghijklm{{/rot13}} \ No newline at end of file
diff --git a/pystache/tests/examples/partial_with_partial_and_lambda.mustache b/pystache/tests/examples/partial_with_partial_and_lambda.mustache
new file mode 100644
index 0000000..0729e10
--- /dev/null
+++ b/pystache/tests/examples/partial_with_partial_and_lambda.mustache
@@ -0,0 +1 @@
+{{>partial_with_lambda}}{{#rot13}}abcdefghijklm{{/rot13}} \ No newline at end of file
diff --git a/pystache/tests/examples/partials_with_lambdas.py b/pystache/tests/examples/partials_with_lambdas.py
new file mode 100644
index 0000000..638aa36
--- /dev/null
+++ b/pystache/tests/examples/partials_with_lambdas.py
@@ -0,0 +1,12 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache.tests.examples.lambdas import rot
+
+class PartialsWithLambdas(object):
+
+ def rot(self):
+ return rot
diff --git a/pystache/tests/examples/readme.py b/pystache/tests/examples/readme.py
new file mode 100644
index 0000000..8dcee43
--- /dev/null
+++ b/pystache/tests/examples/readme.py
@@ -0,0 +1,9 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class SayHello(object):
+ def to(self):
+ return "Pizza"
diff --git a/pystache/tests/examples/say_hello.mustache b/pystache/tests/examples/say_hello.mustache
new file mode 100644
index 0000000..7d8dfea
--- /dev/null
+++ b/pystache/tests/examples/say_hello.mustache
@@ -0,0 +1 @@
+Hello, {{to}}! \ No newline at end of file
diff --git a/pystache/tests/examples/simple.mustache b/pystache/tests/examples/simple.mustache
new file mode 100644
index 0000000..9214dab
--- /dev/null
+++ b/pystache/tests/examples/simple.mustache
@@ -0,0 +1 @@
+Hi {{thing}}!{{blank}} \ No newline at end of file
diff --git a/pystache/tests/examples/simple.py b/pystache/tests/examples/simple.py
new file mode 100644
index 0000000..ea82e9d
--- /dev/null
+++ b/pystache/tests/examples/simple.py
@@ -0,0 +1,15 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class Simple(TemplateSpec):
+
+ def thing(self):
+ return "pizza"
+
+ def blank(self):
+ return ''
diff --git a/pystache/tests/examples/tagless.mustache b/pystache/tests/examples/tagless.mustache
new file mode 100644
index 0000000..ad4dd31
--- /dev/null
+++ b/pystache/tests/examples/tagless.mustache
@@ -0,0 +1 @@
+No tags... \ No newline at end of file
diff --git a/pystache/tests/examples/template_partial.mustache b/pystache/tests/examples/template_partial.mustache
new file mode 100644
index 0000000..03f76cf
--- /dev/null
+++ b/pystache/tests/examples/template_partial.mustache
@@ -0,0 +1,2 @@
+<h1>{{title}}</h1>
+{{>inner_partial}} \ No newline at end of file
diff --git a/pystache/tests/examples/template_partial.py b/pystache/tests/examples/template_partial.py
new file mode 100644
index 0000000..1c4d1a0
--- /dev/null
+++ b/pystache/tests/examples/template_partial.py
@@ -0,0 +1,27 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class TemplatePartial(TemplateSpec):
+
+ def __init__(self, renderer):
+ self.renderer = renderer
+
+ def _context_get(self, key):
+ return self.renderer.context.get(key)
+
+ def title(self):
+ return "Welcome"
+
+ def title_bars(self):
+ return '-' * len(self.title())
+
+ def looping(self):
+ return [{'item': 'one'}, {'item': 'two'}, {'item': 'three'}]
+
+ def thing(self):
+ return self._context_get('prop')
diff --git a/pystache/tests/examples/template_partial.txt b/pystache/tests/examples/template_partial.txt
new file mode 100644
index 0000000..d9b5f6e
--- /dev/null
+++ b/pystache/tests/examples/template_partial.txt
@@ -0,0 +1,4 @@
+{{title}}
+{{title_bars}}
+
+{{>inner_partial}}
diff --git a/pystache/tests/examples/unescaped.mustache b/pystache/tests/examples/unescaped.mustache
new file mode 100644
index 0000000..9982708
--- /dev/null
+++ b/pystache/tests/examples/unescaped.mustache
@@ -0,0 +1 @@
+<h1>{{{title}}}</h1> \ No newline at end of file
diff --git a/pystache/tests/examples/unescaped.py b/pystache/tests/examples/unescaped.py
new file mode 100644
index 0000000..92889af
--- /dev/null
+++ b/pystache/tests/examples/unescaped.py
@@ -0,0 +1,10 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+class Unescaped(object):
+
+ def title(self):
+ return "Bear > Shark"
diff --git a/pystache/tests/examples/unicode_input.mustache b/pystache/tests/examples/unicode_input.mustache
new file mode 100644
index 0000000..f654cd1
--- /dev/null
+++ b/pystache/tests/examples/unicode_input.mustache
@@ -0,0 +1 @@
+abcdé \ No newline at end of file
diff --git a/pystache/tests/examples/unicode_input.py b/pystache/tests/examples/unicode_input.py
new file mode 100644
index 0000000..d045757
--- /dev/null
+++ b/pystache/tests/examples/unicode_input.py
@@ -0,0 +1,14 @@
+
+"""
+TODO: add a docstring.
+
+"""
+
+from pystache import TemplateSpec
+
+class UnicodeInput(TemplateSpec):
+
+ template_encoding = 'utf8'
+
+ def age(self):
+ return 156
diff --git a/pystache/tests/examples/unicode_output.mustache b/pystache/tests/examples/unicode_output.mustache
new file mode 100644
index 0000000..8495f56
--- /dev/null
+++ b/pystache/tests/examples/unicode_output.mustache
@@ -0,0 +1 @@
+<p>Name: {{name}}</p> \ No newline at end of file
diff --git a/pystache/tests/examples/unicode_output.py b/pystache/tests/examples/unicode_output.py
new file mode 100644
index 0000000..da0e1d2
--- /dev/null
+++ b/pystache/tests/examples/unicode_output.py
@@ -0,0 +1,11 @@
+# encoding: utf-8
+
+"""
+TODO: add a docstring.
+
+"""
+
+class UnicodeOutput(object):
+
+ def name(self):
+ return u'Henri Poincaré'
diff --git a/pystache/tests/main.py b/pystache/tests/main.py
new file mode 100644
index 0000000..de56c44
--- /dev/null
+++ b/pystache/tests/main.py
@@ -0,0 +1,155 @@
+# coding: utf-8
+
+"""
+Exposes a run_tests() function that runs all tests in the project.
+
+This module is for our test console script.
+
+"""
+
+import os
+import sys
+import unittest
+from unittest import TestProgram
+
+import pystache
+from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
+from pystache.tests.common import get_module_names
+from pystache.tests.doctesting import get_doctests
+from pystache.tests.spectesting import get_spec_tests
+
+
+# If this command option is present, then the spec test and doctest directories
+# will be inserted if not provided.
+FROM_SOURCE_OPTION = "--from-source"
+
+
+# Do not include "test" in this function's name to avoid it getting
+# picked up by nosetests.
+def main(sys_argv):
+ """
+ Run all tests in the project.
+
+ Arguments:
+
+ sys_argv: a reference to sys.argv.
+
+ """
+ should_source_exist = False
+ spec_test_dir = None
+ project_dir = None
+
+ if len(sys_argv) > 1 and sys_argv[1] == FROM_SOURCE_OPTION:
+ should_source_exist = True
+ sys_argv.pop(1)
+
+ # TODO: use logging module
+ print "pystache: running tests: expecting source: %s" % should_source_exist
+
+ try:
+ # TODO: use optparse command options instead.
+ spec_test_dir = sys_argv[1]
+ sys_argv.pop(1)
+ except IndexError:
+ if should_source_exist:
+ spec_test_dir = SPEC_TEST_DIR
+
+ try:
+ # TODO: use optparse command options instead.
+ project_dir = sys_argv[1]
+ sys_argv.pop(1)
+ except IndexError:
+ if should_source_exist:
+ project_dir = PROJECT_DIR
+
+ if len(sys_argv) <= 1 or sys_argv[-1].startswith("-"):
+ # Then no explicit module or test names were provided, so
+ # auto-detect all unit tests.
+ module_names = _discover_test_modules(PACKAGE_DIR)
+ sys_argv.extend(module_names)
+ if project_dir is not None:
+ # 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
+
+ # 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)
+ # No need to return since unitttest.main() exits.
+
+
+def _discover_test_modules(package_dir):
+ """
+ Discover and return a sorted list of the names of unit-test modules.
+
+ """
+ def is_unittest_module(path):
+ file_name = os.path.basename(path)
+ return file_name.startswith(UNITTEST_FILE_PREFIX)
+
+ names = get_module_names(package_dir=package_dir, should_include=is_unittest_module)
+
+ # This is a sanity check to ensure that the unit-test discovery
+ # methods are working.
+ if len(names) < 1:
+ raise Exception("No unit-test modules found--\n in %s" % package_dir)
+
+ return names
+
+
+class SetupTests(unittest.TestCase):
+
+ """Tests about setup.py."""
+
+ project_dir = None
+
+ def test_version(self):
+ """
+ Test that setup.py's version matches the package's version.
+
+ """
+ original_path = list(sys.path)
+
+ sys.path.insert(0, self.project_dir)
+
+ try:
+ from setup import VERSION
+ 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/spectesting.py b/pystache/tests/spectesting.py
new file mode 100644
index 0000000..ec8a08d
--- /dev/null
+++ b/pystache/tests/spectesting.py
@@ -0,0 +1,285 @@
+# coding: utf-8
+
+"""
+Exposes a get_spec_tests() function for the project's test harness.
+
+Creates a unittest.TestCase for the tests defined in the mustache spec.
+
+"""
+
+# TODO: this module can be cleaned up somewhat.
+# TODO: move all of this code to pystache/tests/spectesting.py and
+# have it expose a get_spec_tests(spec_test_dir) function.
+
+FILE_ENCODING = 'utf-8' # the encoding of the spec test files.
+
+yaml = None
+
+try:
+ # We try yaml first since it is more convenient when adding and modifying
+ # test cases by hand (since the YAML is human-readable and is the master
+ # from which the JSON format is generated).
+ import yaml
+except ImportError:
+ try:
+ import json
+ except:
+ # The module json is not available prior to Python 2.6, whereas
+ # simplejson is. The simplejson package dropped support for Python 2.4
+ # in simplejson v2.1.0, so Python 2.4 requires a simplejson install
+ # older than the most recent version.
+ try:
+ import simplejson as json
+ except ImportError:
+ # Raise an error with a type different from ImportError as a hack around
+ # this issue:
+ # http://bugs.python.org/issue7559
+ from sys import exc_info
+ ex_type, ex_value, tb = exc_info()
+ new_ex = Exception("%s: %s" % (ex_type.__name__, ex_value))
+ raise new_ex.__class__, new_ex, tb
+ file_extension = 'json'
+ parser = json
+else:
+ file_extension = 'yml'
+ parser = yaml
+
+
+import codecs
+import glob
+import os.path
+import unittest
+
+import pystache
+from pystache import common
+from pystache.renderer import Renderer
+from pystache.tests.common import AssertStringMixin
+
+
+def get_spec_tests(spec_test_dir):
+ """
+ Return a list of unittest.TestCase instances.
+
+ """
+ # TODO: use logging module instead.
+ print "pystache: spec tests: using %s" % _get_parser_info()
+
+ cases = []
+
+ # Make this absolute for easier diagnosis in case of error.
+ spec_test_dir = os.path.abspath(spec_test_dir)
+ spec_paths = glob.glob(os.path.join(spec_test_dir, '*.%s' % file_extension))
+
+ for path in spec_paths:
+ new_cases = _read_spec_tests(path)
+ cases.extend(new_cases)
+
+ # Store this as a value so that CheckSpecTestsFound is not checking
+ # a reference to cases that contains itself.
+ spec_test_count = len(cases)
+
+ # This test case lets us alert the user that spec tests are missing.
+ class CheckSpecTestsFound(unittest.TestCase):
+
+ def runTest(self):
+ if spec_test_count > 0:
+ return
+ raise Exception("Spec tests not found--\n in %s\n"
+ " Consult the README file on how to add the Mustache spec tests." % repr(spec_test_dir))
+
+ case = CheckSpecTestsFound()
+ cases.append(case)
+
+ return cases
+
+
+def _get_parser_info():
+ return "%s (version %s)" % (parser.__name__, parser.__version__)
+
+
+def _read_spec_tests(path):
+ """
+ Return a list of unittest.TestCase instances.
+
+ """
+ b = common.read(path)
+ u = unicode(b, encoding=FILE_ENCODING)
+ spec_data = parse(u)
+ tests = spec_data['tests']
+
+ cases = []
+ for data in tests:
+ case = _deserialize_spec_test(data, path)
+ cases.append(case)
+
+ return cases
+
+
+# TODO: simplify the implementation of this function.
+def _convert_children(node):
+ """
+ Recursively convert to functions all "code strings" below the node.
+
+ This function is needed only for the json format.
+
+ """
+ if not isinstance(node, (list, dict)):
+ # Then there is nothing to iterate over and recurse.
+ return
+
+ if isinstance(node, list):
+ for child in node:
+ _convert_children(child)
+ return
+ # Otherwise, node is a dict, so attempt the conversion.
+
+ for key in node.keys():
+ val = node[key]
+
+ if not isinstance(val, dict) or val.get('__tag__') != 'code':
+ _convert_children(val)
+ continue
+ # Otherwise, we are at a "leaf" node.
+
+ val = eval(val['python'])
+ node[key] = val
+ continue
+
+
+def _deserialize_spec_test(data, file_path):
+ """
+ Return a unittest.TestCase instance representing a spec test.
+
+ Arguments:
+
+ data: the dictionary of attributes for a single test.
+
+ """
+ context = data['data']
+ description = data['desc']
+ # PyYAML seems to leave ASCII strings as byte strings.
+ expected = unicode(data['expected'])
+ # TODO: switch to using dict.get().
+ partials = data.has_key('partials') and data['partials'] or {}
+ template = data['template']
+ test_name = data['name']
+
+ _convert_children(context)
+
+ test_case = _make_spec_test(expected, template, context, partials, description, test_name, file_path)
+
+ return test_case
+
+
+def _make_spec_test(expected, template, context, partials, description, test_name, file_path):
+ """
+ Return a unittest.TestCase instance representing a spec test.
+
+ """
+ file_name = os.path.basename(file_path)
+ test_method_name = "Mustache spec (%s): %s" % (file_name, repr(test_name))
+
+ # We subclass SpecTestBase in order to control the test method name (for
+ # the purposes of improved reporting).
+ class SpecTest(SpecTestBase):
+ pass
+
+ def run_test(self):
+ self._runTest()
+
+ # TODO: should we restore this logic somewhere?
+ # If we don't convert unicode to str, we get the following error:
+ # "TypeError: __name__ must be set to a string object"
+ # test.__name__ = str(name)
+ setattr(SpecTest, test_method_name, run_test)
+ case = SpecTest(test_method_name)
+
+ case._context = context
+ case._description = description
+ case._expected = expected
+ case._file_path = file_path
+ case._partials = partials
+ case._template = template
+ case._test_name = test_name
+
+ return case
+
+
+def parse(u):
+ """
+ Parse the contents of a spec test file, and return a dict.
+
+ Arguments:
+
+ u: a unicode string.
+
+ """
+ # TODO: find a cleaner mechanism for choosing between the two.
+ if yaml is None:
+ # Then use json.
+
+ # The only way to get the simplejson module to return unicode strings
+ # is to pass it unicode. See, for example--
+ #
+ # http://code.google.com/p/simplejson/issues/detail?id=40
+ #
+ # and the documentation of simplejson.loads():
+ #
+ # "If s is a str then decoded JSON strings that contain only ASCII
+ # characters may be parsed as str for performance and memory reasons.
+ # If your code expects only unicode the appropriate solution is
+ # decode s to unicode prior to calling loads."
+ #
+ return json.loads(u)
+ # Otherwise, yaml.
+
+ def code_constructor(loader, node):
+ value = loader.construct_mapping(node)
+ return eval(value['python'], {})
+
+ yaml.add_constructor(u'!code', code_constructor)
+ return yaml.load(u)
+
+
+class SpecTestBase(unittest.TestCase, AssertStringMixin):
+
+ def _runTest(self):
+ context = self._context
+ description = self._description
+ expected = self._expected
+ file_path = self._file_path
+ partials = self._partials
+ template = self._template
+ test_name = self._test_name
+
+ renderer = Renderer(partials=partials)
+ actual = renderer.render(template, context)
+
+ # We need to escape the strings that occur in our format string because
+ # they can contain % symbols, for example (in delimiters.yml)--
+ #
+ # "template: '{{=<% %>=}}(<%text%>)'"
+ #
+ def escape(s):
+ return s.replace("%", "%%")
+
+ parser_info = _get_parser_info()
+ subs = [repr(test_name), description, os.path.abspath(file_path),
+ template, repr(context), parser_info]
+ subs = tuple([escape(sub) for sub in subs])
+ # We include the parsing module version info to help with troubleshooting
+ # yaml/json/simplejson issues.
+ message = """%s: %s
+
+ File: %s
+
+ Template: \"""%s\"""
+
+ Context: %s
+
+ %%s
+
+ [using %s]
+ """ % subs
+
+ self.assertString(actual, expected, format=message)
diff --git a/pystache/tests/test___init__.py b/pystache/tests/test___init__.py
new file mode 100644
index 0000000..d4f3526
--- /dev/null
+++ b/pystache/tests/test___init__.py
@@ -0,0 +1,36 @@
+# coding: utf-8
+
+"""
+Tests of __init__.py.
+
+"""
+
+# Calling "import *" is allowed only at the module level.
+GLOBALS_INITIAL = globals().keys()
+from pystache import *
+GLOBALS_PYSTACHE_IMPORTED = globals().keys()
+
+import unittest
+
+import pystache
+
+
+class InitTests(unittest.TestCase):
+
+ def test___all__(self):
+ """
+ Test that "from pystache import *" works as expected.
+
+ """
+ actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
+ expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
+
+ self.assertEqual(actual, expected)
+
+ def test_version_defined(self):
+ """
+ Test that pystache.__version__ is set.
+
+ """
+ actual_version = pystache.__version__
+ self.assertTrue(actual_version)
diff --git a/pystache/tests/test_commands.py b/pystache/tests/test_commands.py
new file mode 100644
index 0000000..2529d25
--- /dev/null
+++ b/pystache/tests/test_commands.py
@@ -0,0 +1,45 @@
+# coding: utf-8
+
+"""
+Unit tests of commands.py.
+
+"""
+
+import sys
+import unittest
+
+from pystache.commands.render import main
+
+
+ORIGINAL_STDOUT = sys.stdout
+
+
+class MockStdout(object):
+
+ def __init__(self):
+ self.output = ""
+
+ def write(self, str):
+ self.output += str
+
+
+class CommandsTestCase(unittest.TestCase):
+
+ def setUp(self):
+ sys.stdout = MockStdout()
+
+ def callScript(self, template, context):
+ argv = ['pystache', template, context]
+ main(argv)
+ return sys.stdout.output
+
+ def testMainSimple(self):
+ """
+ Test a simple command-line case.
+
+ """
+ actual = self.callScript("Hi {{thing}}", '{"thing": "world"}')
+ self.assertEqual(actual, u"Hi world\n")
+
+ def tearDown(self):
+ sys.stdout = ORIGINAL_STDOUT
diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py
new file mode 100644
index 0000000..03377f7
--- /dev/null
+++ b/pystache/tests/test_context.py
@@ -0,0 +1,444 @@
+# coding: utf-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, Attachable
+
+class SimpleObject(object):
+
+ """A sample class that does not define __getitem__()."""
+
+ def __init__(self):
+ self.foo = "bar"
+
+ def foo_callable(self):
+ return "called..."
+
+
+class DictLike(object):
+
+ """A sample class that implements __getitem__() and __contains__()."""
+
+ def __init__(self):
+ self._dict = {'foo': 'bar'}
+ self.fuzz = 'buzz'
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+
+class GetValueTests(unittest.TestCase, AssertIsMixin):
+
+ """Test context._get_value()."""
+
+ def assertNotFound(self, item, key):
+ """
+ Assert that a call to _get_value() returns _NOT_FOUND.
+
+ """
+ self.assertIs(_get_value(item, key), _NOT_FOUND)
+
+ ### Case: the item is a dictionary.
+
+ def test_dictionary__key_present(self):
+ """
+ Test getting a key from a dictionary.
+
+ """
+ item = {"foo": "bar"}
+ self.assertEqual(_get_value(item, "foo"), "bar")
+
+ def test_dictionary__callable_not_called(self):
+ """
+ Test that callable values are returned as-is (and in particular not called).
+
+ """
+ def foo_callable(self):
+ return "bar"
+
+ item = {"foo": foo_callable}
+ self.assertNotEqual(_get_value(item, "foo"), "bar")
+ self.assertTrue(_get_value(item, "foo") is foo_callable)
+
+ def test_dictionary__key_missing(self):
+ """
+ Test getting a missing key from a dictionary.
+
+ """
+ item = {}
+ self.assertNotFound(item, "missing")
+
+ def test_dictionary__attributes_not_checked(self):
+ """
+ Test that dictionary attributes are not checked.
+
+ """
+ item = {1: 2, 3: 4}
+ # I was not able to find a "public" attribute of dict that is
+ # the same across Python 2/3.
+ attr_name = "__len__"
+ self.assertEqual(getattr(item, attr_name)(), 2)
+ self.assertNotFound(item, attr_name)
+
+ def test_dictionary__dict_subclass(self):
+ """
+ Test that subclasses of dict are treated as dictionaries.
+
+ """
+ class DictSubclass(dict): pass
+
+ item = DictSubclass()
+ item["foo"] = "bar"
+
+ self.assertEqual(_get_value(item, "foo"), "bar")
+
+ ### Case: the item is an object.
+
+ def test_object__attribute_present(self):
+ """
+ Test getting an attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertEqual(_get_value(item, "foo"), "bar")
+
+ def test_object__attribute_missing(self):
+ """
+ Test getting a missing attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertNotFound(item, "missing")
+
+ def test_object__attribute_is_callable(self):
+ """
+ Test getting a callable attribute from an object.
+
+ """
+ item = SimpleObject()
+ self.assertEqual(_get_value(item, "foo_callable"), "called...")
+
+ def test_object__non_built_in_type(self):
+ """
+ Test getting an attribute from an instance of a type that isn't built-in.
+
+ """
+ item = datetime(2012, 1, 2)
+ self.assertEqual(_get_value(item, "day"), 2)
+
+ def test_object__dict_like(self):
+ """
+ Test getting a key from a dict-like object (an object that implements '__getitem__').
+
+ """
+ item = DictLike()
+ self.assertEqual(item["foo"], "bar")
+ self.assertNotFound(item, "foo")
+
+ ### Case: the item is an instance of a built-in type.
+
+ def test_built_in_type__integer(self):
+ """
+ Test getting from an integer.
+
+ """
+ class MyInt(int): pass
+
+ cust_int = MyInt(10)
+ pure_int = 10
+
+ # We have to use a built-in method like __neg__ because "public"
+ # attributes like "real" were not added to Python until Python 2.6,
+ # when the numeric type hierarchy was added:
+ #
+ # http://docs.python.org/library/numbers.html
+ #
+ self.assertEqual(cust_int.__neg__(), -10)
+ self.assertEqual(pure_int.__neg__(), -10)
+
+ self.assertEqual(_get_value(cust_int, '__neg__'), -10)
+ self.assertNotFound(pure_int, '__neg__')
+
+ def test_built_in_type__string(self):
+ """
+ Test getting from a string.
+
+ """
+ class MyStr(str): pass
+
+ item1 = MyStr('abc')
+ item2 = 'abc'
+
+ self.assertEqual(item1.upper(), 'ABC')
+ self.assertEqual(item2.upper(), 'ABC')
+
+ self.assertEqual(_get_value(item1, 'upper'), 'ABC')
+ self.assertNotFound(item2, 'upper')
+
+ def test_built_in_type__list(self):
+ """
+ Test getting from a list.
+
+ """
+ class MyList(list): pass
+
+ item1 = MyList([1, 2, 3])
+ item2 = [1, 2, 3]
+
+ self.assertEqual(item1.pop(), 3)
+ self.assertEqual(item2.pop(), 3)
+
+ self.assertEqual(_get_value(item1, 'pop'), 2)
+ self.assertNotFound(item2, 'pop')
+
+
+class ContextStackTests(unittest.TestCase, AssertIsMixin):
+
+ """
+ Test the ContextStack class.
+
+ """
+
+ def test_init__no_elements(self):
+ """
+ Check that passing nothing to __init__() raises no exception.
+
+ """
+ context = ContextStack()
+
+ def test_init__many_elements(self):
+ """
+ Check that passing more than two items to __init__() raises no exception.
+
+ """
+ context = ContextStack({}, {}, {})
+
+ def test__repr(self):
+ context = ContextStack()
+ self.assertEqual(repr(context), 'ContextStack()')
+
+ context = ContextStack({'foo': 'bar'})
+ self.assertEqual(repr(context), "ContextStack({'foo': 'bar'},)")
+
+ context = ContextStack({'foo': 'bar'}, {'abc': 123})
+ self.assertEqual(repr(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
+
+ def test__str(self):
+ context = ContextStack()
+ self.assertEqual(str(context), 'ContextStack()')
+
+ context = ContextStack({'foo': 'bar'})
+ self.assertEqual(str(context), "ContextStack({'foo': 'bar'},)")
+
+ context = ContextStack({'foo': 'bar'}, {'abc': 123})
+ self.assertEqual(str(context), "ContextStack({'foo': 'bar'}, {'abc': 123})")
+
+ ## Test the static create() method.
+
+ def test_create__dictionary(self):
+ """
+ Test passing a dictionary.
+
+ """
+ context = ContextStack.create({'foo': 'bar'})
+ self.assertEqual(context.get('foo'), 'bar')
+
+ def test_create__none(self):
+ """
+ Test passing None.
+
+ """
+ context = ContextStack.create({'foo': 'bar'}, None)
+ self.assertEqual(context.get('foo'), 'bar')
+
+ def test_create__object(self):
+ """
+ Test passing an object.
+
+ """
+ class Foo(object):
+ foo = 'bar'
+ context = ContextStack.create(Foo())
+ self.assertEqual(context.get('foo'), 'bar')
+
+ def test_create__context(self):
+ """
+ Test passing a ContextStack instance.
+
+ """
+ obj = ContextStack({'foo': 'bar'})
+ context = ContextStack.create(obj)
+ self.assertEqual(context.get('foo'), 'bar')
+
+ def test_create__kwarg(self):
+ """
+ Test passing a keyword argument.
+
+ """
+ context = ContextStack.create(foo='bar')
+ self.assertEqual(context.get('foo'), 'bar')
+
+ def test_create__precedence_positional(self):
+ """
+ Test precedence of positional arguments.
+
+ """
+ context = ContextStack.create({'foo': 'bar'}, {'foo': 'buzz'})
+ self.assertEqual(context.get('foo'), 'buzz')
+
+ def test_create__precedence_keyword(self):
+ """
+ Test precedence of keyword arguments.
+
+ """
+ context = ContextStack.create({'foo': 'bar'}, foo='buzz')
+ self.assertEqual(context.get('foo'), 'buzz')
+
+ def test_get__key_present(self):
+ """
+ Test getting a key.
+
+ """
+ context = ContextStack({"foo": "bar"})
+ self.assertEqual(context.get("foo"), "bar")
+
+ def test_get__key_missing(self):
+ """
+ Test getting a missing key.
+
+ """
+ context = ContextStack()
+ self.assertTrue(context.get("foo") is None)
+
+ 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):
+ """
+ Test that get() respects the order of precedence (later items first).
+
+ """
+ context = ContextStack({"foo": "bar"}, {"foo": "buzz"})
+ self.assertEqual(context.get("foo"), "buzz")
+
+ def test_get__fallback(self):
+ """
+ Check that first-added stack items are queried on context misses.
+
+ """
+ context = ContextStack({"fuzz": "buzz"}, {"foo": "bar"})
+ self.assertEqual(context.get("fuzz"), "buzz")
+
+ def test_push(self):
+ """
+ Test push().
+
+ """
+ key = "foo"
+ context = ContextStack({key: "bar"})
+ self.assertEqual(context.get(key), "bar")
+
+ context.push({key: "buzz"})
+ self.assertEqual(context.get(key), "buzz")
+
+ def test_pop(self):
+ """
+ Test pop().
+
+ """
+ key = "foo"
+ context = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(context.get(key), "buzz")
+
+ item = context.pop()
+ self.assertEqual(item, {"foo": "buzz"})
+ self.assertEqual(context.get(key), "bar")
+
+ def test_top(self):
+ key = "foo"
+ context = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(context.get(key), "buzz")
+
+ top = context.top()
+ self.assertEqual(top, {"foo": "buzz"})
+ # Make sure calling top() didn't remove the item from the stack.
+ self.assertEqual(context.get(key), "buzz")
+
+ def test_copy(self):
+ key = "foo"
+ original = ContextStack({key: "bar"}, {key: "buzz"})
+ self.assertEqual(original.get(key), "buzz")
+
+ new = original.copy()
+ # Confirm that the copy behaves the same.
+ self.assertEqual(new.get(key), "buzz")
+ # Change the copy, and confirm it is changed.
+ new.pop()
+ self.assertEqual(new.get(key), "bar")
+ # Confirm the original is unchanged.
+ self.assertEqual(original.get(key), "buzz")
+
+ def test_dot_notation__dict(self):
+ key = "foo.bar"
+ original = ContextStack({"foo": {"bar": "baz"}})
+ self.assertEquals(original.get(key), "baz")
+
+ # Works all the way down
+ key = "a.b.c.d.e.f.g"
+ original = ContextStack({"a": {"b": {"c": {"d": {"e": {"f": {"g": "w00t!"}}}}}}})
+ self.assertEquals(original.get(key), "w00t!")
+
+ def test_dot_notation__user_object(self):
+ key = "foo.bar"
+ original = ContextStack({"foo": Attachable(bar="baz")})
+ self.assertEquals(original.get(key), "baz")
+
+ # Works on multiple levels, too
+ key = "a.b.c.d.e.f.g"
+ Obj = Attachable
+ original = ContextStack({"a": Obj(b=Obj(c=Obj(d=Obj(e=Obj(f=Obj(g="w00t!"))))))})
+ self.assertEquals(original.get(key), "w00t!")
+
+ def test_dot_notation__mixed_dict_and_obj(self):
+ key = "foo.bar.baz.bak"
+ original = ContextStack({"foo": Attachable(bar={"baz": Attachable(bak=42)})})
+ self.assertEquals(original.get(key), 42)
+
+ def test_dot_notation__missing_attr_or_key(self):
+ key = "foo.bar.baz.bak"
+ original = ContextStack({"foo": {"bar": {}}})
+ self.assertEquals(original.get(key), None)
+
+ original = ContextStack({"foo": Attachable(bar=Attachable())})
+ self.assertEquals(original.get(key), None)
+
+ def test_dot_notattion__autocall(self):
+ key = "foo.bar.baz"
+
+ # When any element in the path is callable, it should be automatically invoked
+ original = ContextStack({"foo": Attachable(bar=Attachable(baz=lambda: "Called!"))})
+ self.assertEquals(original.get(key), "Called!")
+
+ class Foo(object):
+ def bar(self):
+ return Attachable(baz='Baz')
+
+ original = ContextStack({"foo": Foo()})
+ self.assertEquals(original.get(key), "Baz")
diff --git a/pystache/tests/test_examples.py b/pystache/tests/test_examples.py
new file mode 100644
index 0000000..5c9f74d
--- /dev/null
+++ b/pystache/tests/test_examples.py
@@ -0,0 +1,106 @@
+# encoding: utf-8
+
+"""
+TODO: add a docstring.
+
+"""
+
+import unittest
+
+from examples.comments import Comments
+from examples.double_section import DoubleSection
+from examples.escaped import Escaped
+from examples.unescaped import Unescaped
+from examples.template_partial import TemplatePartial
+from examples.delimiters import Delimiters
+from examples.unicode_output import UnicodeOutput
+from examples.unicode_input import UnicodeInput
+from examples.nested_context import NestedContext
+from pystache import Renderer
+from pystache.tests.common import EXAMPLES_DIR
+from pystache.tests.common import AssertStringMixin
+
+
+class TestView(unittest.TestCase, AssertStringMixin):
+
+ def _assert(self, obj, expected):
+ renderer = Renderer()
+ actual = renderer.render(obj)
+ self.assertString(actual, expected)
+
+ def test_comments(self):
+ self._assert(Comments(), u"<h1>A Comedy of Errors</h1>")
+
+ def test_double_section(self):
+ self._assert(DoubleSection(), u"* first\n* second\n* third")
+
+ def test_unicode_output(self):
+ renderer = Renderer()
+ actual = renderer.render(UnicodeOutput())
+ self.assertString(actual, u'<p>Name: Henri Poincaré</p>')
+
+ def test_unicode_input(self):
+ renderer = Renderer()
+ actual = renderer.render(UnicodeInput())
+ self.assertString(actual, u'abcdé')
+
+ def test_escaping(self):
+ self._assert(Escaped(), u"<h1>Bear &gt; Shark</h1>")
+
+ def test_literal(self):
+ renderer = Renderer()
+ actual = renderer.render(Unescaped())
+ self.assertString(actual, u"<h1>Bear > Shark</h1>")
+
+ def test_template_partial(self):
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(TemplatePartial(renderer=renderer))
+
+ self.assertString(actual, u"""<h1>Welcome</h1>
+Again, Welcome!""")
+
+ def test_template_partial_extension(self):
+ renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
+
+ view = TemplatePartial(renderer=renderer)
+
+ actual = renderer.render(view)
+ self.assertString(actual, u"""Welcome
+-------
+
+## Again, Welcome! ##""")
+
+ def test_delimiters(self):
+ renderer = Renderer()
+ actual = renderer.render(Delimiters())
+ self.assertString(actual, u"""\
+* It worked the first time.
+* And it worked the second time.
+* Then, surprisingly, it worked the third time.
+""")
+
+ def test_nested_context(self):
+ renderer = Renderer()
+ actual = renderer.render(NestedContext(renderer))
+ self.assertString(actual, u"one and foo and two")
+
+ def test_nested_context_is_available_in_view(self):
+ renderer = Renderer()
+
+ view = NestedContext(renderer)
+ view.template = '{{#herp}}{{#derp}}{{nested_context_in_view}}{{/derp}}{{/herp}}'
+
+ actual = renderer.render(view)
+ self.assertString(actual, u'it works!')
+
+ def test_partial_in_partial_has_access_to_grand_parent_context(self):
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+
+ view = TemplatePartial(renderer=renderer)
+ view.template = '''{{>partial_in_partial}}'''
+
+ actual = renderer.render(view, {'prop': 'derp'})
+ self.assertEqual(actual, 'Hi derp!')
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pystache/tests/test_loader.py b/pystache/tests/test_loader.py
new file mode 100644
index 0000000..c47239c
--- /dev/null
+++ b/pystache/tests/test_loader.py
@@ -0,0 +1,194 @@
+# encoding: utf-8
+
+"""
+Unit tests of loader.py.
+
+"""
+
+import os
+import sys
+import unittest
+
+from pystache.tests.common import AssertStringMixin, DATA_DIR, SetupDefaults
+from pystache import defaults
+from pystache.loader import Loader
+
+
+class LoaderTests(unittest.TestCase, AssertStringMixin, SetupDefaults):
+
+ def setUp(self):
+ self.setup_defaults()
+
+ def tearDown(self):
+ self.teardown_defaults()
+
+ def test_init__extension(self):
+ loader = Loader(extension='foo')
+ self.assertEqual(loader.extension, 'foo')
+
+ def test_init__extension__default(self):
+ # Test the default value.
+ loader = Loader()
+ self.assertEqual(loader.extension, 'mustache')
+
+ def test_init__file_encoding(self):
+ loader = Loader(file_encoding='bar')
+ self.assertEqual(loader.file_encoding, 'bar')
+
+ def test_init__file_encoding__default(self):
+ file_encoding = defaults.FILE_ENCODING
+ try:
+ defaults.FILE_ENCODING = 'foo'
+ loader = Loader()
+ self.assertEqual(loader.file_encoding, 'foo')
+ finally:
+ defaults.FILE_ENCODING = file_encoding
+
+ def test_init__to_unicode(self):
+ to_unicode = lambda x: x
+ loader = Loader(to_unicode=to_unicode)
+ self.assertEqual(loader.to_unicode, to_unicode)
+
+ def test_init__to_unicode__default(self):
+ loader = Loader()
+ self.assertRaises(TypeError, loader.to_unicode, u"abc")
+
+ decode_errors = defaults.DECODE_ERRORS
+ string_encoding = defaults.STRING_ENCODING
+
+ nonascii = u'abcdé'.encode('utf-8')
+
+ loader = Loader()
+ self.assertRaises(UnicodeDecodeError, loader.to_unicode, nonascii)
+
+ defaults.DECODE_ERRORS = 'ignore'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcd')
+
+ defaults.STRING_ENCODING = 'utf-8'
+ loader = Loader()
+ self.assertString(loader.to_unicode(nonascii), u'abcdé')
+
+
+ def _get_path(self, filename):
+ return os.path.join(DATA_DIR, filename)
+
+ def test_unicode__basic__input_str(self):
+ """
+ Test unicode(): default arguments with str input.
+
+ """
+ loader = Loader()
+ actual = loader.unicode("foo")
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__basic__input_unicode(self):
+ """
+ Test unicode(): default arguments with unicode input.
+
+ """
+ loader = Loader()
+ actual = loader.unicode(u"foo")
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__basic__input_unicode_subclass(self):
+ """
+ Test unicode(): default arguments with unicode-subclass input.
+
+ """
+ class UnicodeSubclass(unicode):
+ pass
+
+ s = UnicodeSubclass(u"foo")
+
+ loader = Loader()
+ actual = loader.unicode(s)
+
+ self.assertString(actual, u"foo")
+
+ def test_unicode__to_unicode__attribute(self):
+ """
+ Test unicode(): encoding attribute.
+
+ """
+ loader = Loader()
+
+ non_ascii = u'abcdé'.encode('utf-8')
+ self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
+
+ def to_unicode(s, encoding=None):
+ if encoding is None:
+ encoding = 'utf-8'
+ return unicode(s, encoding)
+
+ loader.to_unicode = to_unicode
+ self.assertString(loader.unicode(non_ascii), u"abcdé")
+
+ def test_unicode__encoding_argument(self):
+ """
+ Test unicode(): encoding argument.
+
+ """
+ loader = Loader()
+
+ non_ascii = u'abcdé'.encode('utf-8')
+
+ self.assertRaises(UnicodeDecodeError, loader.unicode, non_ascii)
+
+ actual = loader.unicode(non_ascii, encoding='utf-8')
+ self.assertString(actual, u'abcdé')
+
+ # TODO: check the read() unit tests.
+ def test_read(self):
+ """
+ Test read().
+
+ """
+ loader = Loader()
+ path = self._get_path('ascii.mustache')
+ actual = loader.read(path)
+ self.assertString(actual, u'ascii: abc')
+
+ def test_read__file_encoding__attribute(self):
+ """
+ Test read(): file_encoding attribute respected.
+
+ """
+ loader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, loader.read, path)
+
+ loader.file_encoding = 'utf-8'
+ actual = loader.read(path)
+ self.assertString(actual, u'non-ascii: é')
+
+ def test_read__encoding__argument(self):
+ """
+ Test read(): encoding argument respected.
+
+ """
+ loader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, loader.read, path)
+
+ actual = loader.read(path, encoding='utf-8')
+ self.assertString(actual, u'non-ascii: é')
+
+ def test_loader__to_unicode__attribute(self):
+ """
+ Test read(): to_unicode attribute respected.
+
+ """
+ loader = Loader()
+ path = self._get_path('non_ascii.mustache')
+
+ self.assertRaises(UnicodeDecodeError, loader.read, path)
+
+ #loader.decode_errors = 'ignore'
+ #actual = loader.read(path)
+ #self.assertString(actual, u'non-ascii: ')
+
diff --git a/pystache/tests/test_locator.py b/pystache/tests/test_locator.py
new file mode 100644
index 0000000..3a8b229
--- /dev/null
+++ b/pystache/tests/test_locator.py
@@ -0,0 +1,157 @@
+# encoding: utf-8
+
+"""
+Unit tests for locator.py.
+
+"""
+
+from datetime import datetime
+import os
+import sys
+import unittest
+
+# TODO: remove this alias.
+from pystache.loader import Loader as Reader
+from pystache.locator import Locator
+
+from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
+from pystache.tests.data.views import SayHello
+
+
+class LocatorTests(unittest.TestCase):
+
+ def _locator(self):
+ return Locator(search_dirs=DATA_DIR)
+
+ def test_init__extension(self):
+ # Test the default value.
+ locator = Locator()
+ self.assertEqual(locator.template_extension, 'mustache')
+
+ locator = Locator(extension='txt')
+ self.assertEqual(locator.template_extension, 'txt')
+
+ locator = Locator(extension=False)
+ self.assertTrue(locator.template_extension is False)
+
+ def _assert_paths(self, actual, expected):
+ """
+ Assert that two paths are the same.
+
+ """
+ self.assertEqual(actual, expected)
+
+ def test_get_object_directory(self):
+ locator = Locator()
+
+ obj = SayHello()
+ actual = locator.get_object_directory(obj)
+
+ self._assert_paths(actual, DATA_DIR)
+
+ def test_get_object_directory__not_hasattr_module(self):
+ locator = Locator()
+
+ obj = datetime(2000, 1, 1)
+ self.assertFalse(hasattr(obj, '__module__'))
+ self.assertEqual(locator.get_object_directory(obj), None)
+
+ self.assertFalse(hasattr(None, '__module__'))
+ self.assertEqual(locator.get_object_directory(None), None)
+
+ def test_make_file_name(self):
+ locator = Locator()
+
+ locator.template_extension = 'bar'
+ self.assertEqual(locator.make_file_name('foo'), 'foo.bar')
+
+ locator.template_extension = False
+ self.assertEqual(locator.make_file_name('foo'), 'foo')
+
+ locator.template_extension = ''
+ self.assertEqual(locator.make_file_name('foo'), 'foo.')
+
+ def test_make_file_name__template_extension_argument(self):
+ locator = Locator()
+
+ self.assertEqual(locator.make_file_name('foo', template_extension='bar'), 'foo.bar')
+
+ def test_find_name(self):
+ locator = Locator()
+ path = locator.find_name(search_dirs=[EXAMPLES_DIR], template_name='simple')
+
+ self.assertEqual(os.path.basename(path), 'simple.mustache')
+
+ def test_find_name__using_list_of_paths(self):
+ locator = Locator()
+ path = locator.find_name(search_dirs=[EXAMPLES_DIR, 'doesnt_exist'], template_name='simple')
+
+ self.assertTrue(path)
+
+ def test_find_name__precedence(self):
+ """
+ Test the order in which find_name() searches directories.
+
+ """
+ locator = Locator()
+
+ dir1 = DATA_DIR
+ dir2 = os.path.join(DATA_DIR, 'locator')
+
+ self.assertTrue(locator.find_name(search_dirs=[dir1], template_name='duplicate'))
+ self.assertTrue(locator.find_name(search_dirs=[dir2], template_name='duplicate'))
+
+ path = locator.find_name(search_dirs=[dir2, dir1], template_name='duplicate')
+ dirpath = os.path.dirname(path)
+ dirname = os.path.split(dirpath)[-1]
+
+ self.assertEqual(dirname, 'locator')
+
+ def test_find_name__non_existent_template_fails(self):
+ locator = Locator()
+
+ self.assertRaises(IOError, locator.find_name, search_dirs=[], template_name='doesnt_exist')
+
+ def test_find_object(self):
+ locator = Locator()
+
+ obj = SayHello()
+
+ actual = locator.find_object(search_dirs=[], obj=obj, file_name='sample_view.mustache')
+ expected = os.path.join(DATA_DIR, 'sample_view.mustache')
+
+ self._assert_paths(actual, expected)
+
+ def test_find_object__none_file_name(self):
+ locator = Locator()
+
+ obj = SayHello()
+
+ actual = locator.find_object(search_dirs=[], obj=obj)
+ expected = os.path.join(DATA_DIR, 'say_hello.mustache')
+
+ self.assertEqual(actual, expected)
+
+ def test_find_object__none_object_directory(self):
+ locator = Locator()
+
+ obj = None
+ self.assertEqual(None, locator.get_object_directory(obj))
+
+ actual = locator.find_object(search_dirs=[DATA_DIR], obj=obj, file_name='say_hello.mustache')
+ expected = os.path.join(DATA_DIR, 'say_hello.mustache')
+
+ self.assertEqual(actual, expected)
+
+ def test_make_template_name(self):
+ """
+ Test make_template_name().
+
+ """
+ locator = Locator()
+
+ class FooBar(object):
+ pass
+ foo = FooBar()
+
+ self.assertEqual(locator.make_template_name(foo), 'foo_bar')
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
new file mode 100644
index 0000000..4aa0959
--- /dev/null
+++ b/pystache/tests/test_parser.py
@@ -0,0 +1,26 @@
+# coding: utf-8
+
+"""
+Unit tests of parser.py.
+
+"""
+
+import unittest
+
+from pystache.parser import _compile_template_re as make_re
+
+
+class RegularExpressionTestCase(unittest.TestCase):
+
+ """Tests the regular expression returned by _compile_template_re()."""
+
+ def test_re(self):
+ """
+ Test getting a key from a dictionary.
+
+ """
+ re = make_re()
+ match = re.search("b {{test}}")
+
+ self.assertEqual(match.start(), 1)
+
diff --git a/pystache/tests/test_pystache.py b/pystache/tests/test_pystache.py
new file mode 100644
index 0000000..5447f8d
--- /dev/null
+++ b/pystache/tests/test_pystache.py
@@ -0,0 +1,127 @@
+# encoding: utf-8
+
+import unittest
+
+import pystache
+from pystache import defaults
+from pystache import renderer
+from pystache.tests.common import html_escape
+
+
+class PystacheTests(unittest.TestCase):
+
+
+ def setUp(self):
+ self.original_escape = defaults.TAG_ESCAPE
+ defaults.TAG_ESCAPE = html_escape
+
+ def tearDown(self):
+ defaults.TAG_ESCAPE = self.original_escape
+
+ def _assert_rendered(self, expected, template, context):
+ actual = pystache.render(template, context)
+ self.assertEqual(actual, expected)
+
+ def test_basic(self):
+ ret = pystache.render("Hi {{thing}}!", { 'thing': 'world' })
+ self.assertEqual(ret, "Hi world!")
+
+ def test_kwargs(self):
+ ret = pystache.render("Hi {{thing}}!", thing='world')
+ self.assertEqual(ret, "Hi world!")
+
+ def test_less_basic(self):
+ template = "It's a nice day for {{beverage}}, right {{person}}?"
+ context = { 'beverage': 'soda', 'person': 'Bob' }
+ self._assert_rendered("It's a nice day for soda, right Bob?", template, context)
+
+ def test_even_less_basic(self):
+ template = "I think {{name}} wants a {{thing}}, right {{name}}?"
+ context = { 'name': 'Jon', 'thing': 'racecar' }
+ self._assert_rendered("I think Jon wants a racecar, right Jon?", template, context)
+
+ def test_ignores_misses(self):
+ template = "I think {{name}} wants a {{thing}}, right {{name}}?"
+ context = { 'name': 'Jon' }
+ self._assert_rendered("I think Jon wants a , right Jon?", template, context)
+
+ def test_render_zero(self):
+ template = 'My value is {{value}}.'
+ context = { 'value': 0 }
+ self._assert_rendered('My value is 0.', template, context)
+
+ def test_comments(self):
+ template = "What {{! the }} what?"
+ actual = pystache.render(template)
+ self.assertEqual("What what?", actual)
+
+ def test_false_sections_are_hidden(self):
+ template = "Ready {{#set}}set {{/set}}go!"
+ context = { 'set': False }
+ self._assert_rendered("Ready go!", template, context)
+
+ def test_true_sections_are_shown(self):
+ template = "Ready {{#set}}set{{/set}} go!"
+ context = { 'set': True }
+ self._assert_rendered("Ready set go!", template, context)
+
+ non_strings_expected = """(123 & [&#x27;something&#x27;])(chris & 0.9)"""
+
+ def test_non_strings(self):
+ template = "{{#stats}}({{key}} & {{value}}){{/stats}}"
+ stats = []
+ stats.append({'key': 123, 'value': ['something']})
+ stats.append({'key': u"chris", 'value': 0.900})
+ context = { 'stats': stats }
+ self._assert_rendered(self.non_strings_expected, template, context)
+
+ def test_unicode(self):
+ template = 'Name: {{name}}; Age: {{age}}'
+ context = {'name': u'Henri Poincaré', 'age': 156 }
+ self._assert_rendered(u'Name: Henri Poincaré; Age: 156', template, context)
+
+ def test_sections(self):
+ template = """<ul>{{#users}}<li>{{name}}</li>{{/users}}</ul>"""
+
+ context = { 'users': [ {'name': 'Chris'}, {'name': 'Tom'}, {'name': 'PJ'} ] }
+ expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
+ self._assert_rendered(expected, template, context)
+
+ def test_implicit_iterator(self):
+ template = """<ul>{{#users}}<li>{{.}}</li>{{/users}}</ul>"""
+ context = { 'users': [ 'Chris', 'Tom','PJ' ] }
+ expected = """<ul><li>Chris</li><li>Tom</li><li>PJ</li></ul>"""
+ self._assert_rendered(expected, template, context)
+
+ # The spec says that sections should not alter surrounding whitespace.
+ def test_surrounding_whitepace_not_altered(self):
+ template = "first{{#spacing}} second {{/spacing}}third"
+ context = {"spacing": True}
+ self._assert_rendered("first second third", template, context)
+
+ def test__section__non_false_value(self):
+ """
+ Test when a section value is a (non-list) "non-false value".
+
+ From mustache(5):
+
+ When the value [of a section key] is non-false but not a list, it
+ will be used as the context for a single rendering of the block.
+
+ """
+ template = """{{#person}}Hi {{name}}{{/person}}"""
+ context = {"person": {"name": "Jon"}}
+ self._assert_rendered("Hi Jon", template, context)
+
+ def test_later_list_section_with_escapable_character(self):
+ """
+ This is a simple test case intended to cover issue #53.
+
+ The test case failed with markupsafe enabled, as follows:
+
+ AssertionError: Markup(u'foo &lt;') != 'foo <'
+
+ """
+ template = """{{#s1}}foo{{/s1}} {{#s2}}<{{/s2}}"""
+ context = {'s1': True, 's2': [True]}
+ self._assert_rendered("foo <", template, context)
diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
new file mode 100644
index 0000000..2fcb95b
--- /dev/null
+++ b/pystache/tests/test_renderengine.py
@@ -0,0 +1,610 @@
+# coding: utf-8
+
+"""
+Unit tests of renderengine.py.
+
+"""
+
+import unittest
+
+from pystache.context import ContextStack
+from pystache import defaults
+from pystache.parser import ParsingError
+from pystache.renderengine import RenderEngine
+from pystache.tests.common import AssertStringMixin, Attachable
+
+
+def mock_literal(s):
+ """
+ For use as the literal keyword argument to the RenderEngine constructor.
+
+ Arguments:
+
+ s: a byte string or unicode string.
+
+ """
+ if isinstance(s, unicode):
+ # Strip off unicode super classes, if present.
+ u = unicode(s)
+ else:
+ u = unicode(s, encoding='ascii')
+
+ # We apply upper() to make sure we are actually using our custom
+ # function in the tests
+ return u.upper()
+
+
+
+class RenderEngineTestCase(unittest.TestCase):
+
+ """Test the RenderEngine class."""
+
+ def test_init(self):
+ """
+ Test that __init__() stores all of the arguments correctly.
+
+ """
+ # In real-life, these arguments would be functions
+ engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
+
+ self.assertEqual(engine.escape, "escape")
+ self.assertEqual(engine.literal, "literal")
+ self.assertEqual(engine.load_partial, "foo")
+
+
+class RenderTests(unittest.TestCase, AssertStringMixin):
+
+ """
+ Tests RenderEngine.render().
+
+ Explicit spec-test-like tests best go in this class since the
+ RenderEngine class contains all parsing logic. This way, the unit tests
+ will be more focused and fail "closer to the code".
+
+ """
+
+ def _engine(self):
+ """
+ Create and return a default RenderEngine for testing.
+
+ """
+ escape = defaults.TAG_ESCAPE
+ engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ return engine
+
+ def _assert_render(self, expected, template, *context, **kwargs):
+ """
+ Test rendering the given template using the given context.
+
+ """
+ partials = kwargs.get('partials')
+ engine = kwargs.get('engine', self._engine())
+
+ if partials is not None:
+ engine.load_partial = lambda key: unicode(partials[key])
+
+ context = ContextStack(*context)
+
+ actual = engine.render(template, context)
+
+ self.assertString(actual=actual, expected=expected)
+
+ def test_render(self):
+ self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
+
+ def test__load_partial(self):
+ """
+ Test that render() uses the load_template attribute.
+
+ """
+ engine = self._engine()
+ partials = {'partial': u"{{person}}"}
+ engine.load_partial = lambda key: partials[key]
+
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
+
+ def test__literal(self):
+ """
+ Test that render() uses the literal attribute.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper()
+
+ self._assert_render(u'BAR', '{{{foo}}}', {'foo': 'bar'}, engine=engine)
+
+ def test_literal__sigil(self):
+ template = "<h1>{{& thing}}</h1>"
+ context = {'thing': 'Bear > Giraffe'}
+
+ expected = u"<h1>Bear > Giraffe</h1>"
+
+ self._assert_render(expected, template, context)
+
+ def test__escape(self):
+ """
+ Test that render() uses the escape attribute.
+
+ """
+ engine = self._engine()
+ engine.escape = lambda s: "**" + s
+
+ self._assert_render(u'**bar', '{{foo}}', {'foo': 'bar'}, engine=engine)
+
+ def test__escape_does_not_call_literal(self):
+ """
+ Test that render() does not call literal before or after calling escape.
+
+ """
+ engine = self._engine()
+ engine.literal = lambda s: s.upper() # a test version
+ engine.escape = lambda s: "**" + s
+
+ template = 'literal: {{{foo}}} escaped: {{foo}}'
+ context = {'foo': 'bar'}
+
+ self._assert_render(u'literal: BAR escaped: **bar', template, context, engine=engine)
+
+ def test__escape_preserves_unicode_subclasses(self):
+ """
+ Test that render() preserves unicode subclasses when passing to escape.
+
+ This is useful, for example, if one wants to respect whether a
+ variable value is markupsafe.Markup when escaping.
+
+ """
+ class MyUnicode(unicode):
+ pass
+
+ def escape(s):
+ if type(s) is MyUnicode:
+ return "**" + s
+ else:
+ return s + "**"
+
+ engine = self._engine()
+ engine.escape = escape
+
+ template = '{{foo1}} {{foo2}}'
+ context = {'foo1': MyUnicode('bar'), 'foo2': 'bar'}
+
+ self._assert_render(u'**bar bar**', template, context, engine=engine)
+
+ def test__non_basestring__literal_and_escaped(self):
+ """
+ Test a context value that is not a basestring instance.
+
+ """
+ engine = self._engine()
+ engine.escape = mock_literal
+ engine.literal = mock_literal
+
+ self.assertRaises(TypeError, engine.literal, 100)
+
+ template = '{{text}} {{int}} {{{int}}}'
+ context = {'int': 100, 'text': 'foo'}
+
+ self._assert_render(u'FOO 100 100', template, context, engine=engine)
+
+ def test_tag__output_not_interpolated(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{template}}: {{planet}}'
+ context = {'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ def test_tag__output_not_interpolated__section(self):
+ """
+ Context values should not be treated as templates (issue #44).
+
+ """
+ template = '{{test}}'
+ context = {'test': '{{#hello}}'}
+ self._assert_render(u'{{#hello}}', template, context)
+
+ ## Test interpolation with "falsey" values
+ #
+ # In these test cases, we test the part of the spec that says that
+ # "data should be coerced into a string (and escaped, if appropriate)
+ # before interpolation." We test this for data that is "falsey."
+
+ def test_interpolation__falsey__zero(self):
+ template = '{{.}}'
+ context = 0
+ self._assert_render(u'0', template, context)
+
+ def test_interpolation__falsey__none(self):
+ template = '{{.}}'
+ context = None
+ self._assert_render(u'None', template, context)
+
+ def test_interpolation__falsey__zero(self):
+ template = '{{.}}'
+ context = False
+ self._assert_render(u'False', template, context)
+
+ # Built-in types:
+ #
+ # Confirm that we not treat instances of built-in types as objects,
+ # for example by calling a method on a built-in type instance when it
+ # has a method whose name matches the current key.
+ #
+ # Each test case puts an instance of a built-in type on top of the
+ # context stack before interpolating a tag whose key matches an
+ # attribute (method or property) of the instance.
+ #
+
+ def _assert_builtin_attr(self, item, attr_name, expected_attr):
+ self.assertTrue(hasattr(item, attr_name))
+ actual = getattr(item, attr_name)
+ if callable(actual):
+ actual = actual()
+ self.assertEqual(actual, expected_attr)
+
+ def _assert_builtin_type(self, item, attr_name, expected_attr, expected_template):
+ self._assert_builtin_attr(item, attr_name, expected_attr)
+
+ template = '{{#section}}{{%s}}{{/section}}' % attr_name
+ context = {'section': item, attr_name: expected_template}
+ self._assert_render(expected_template, template, context)
+
+ def test_interpolation__built_in_type__string(self):
+ """
+ Check tag interpolation with a built-in type: string.
+
+ """
+ self._assert_builtin_type('abc', 'upper', 'ABC', u'xyz')
+
+ def test_interpolation__built_in_type__integer(self):
+ """
+ Check tag interpolation with a built-in type: integer.
+
+ """
+ # Since public attributes weren't added to integers until Python 2.6
+ # (for example the "real" attribute of the numeric type hierarchy)--
+ #
+ # http://docs.python.org/library/numbers.html
+ #
+ # we need to resort to built-in attributes (double-underscored) on
+ # the integer type.
+ self._assert_builtin_type(15, '__neg__', -15, u'999')
+
+ def test_interpolation__built_in_type__list(self):
+ """
+ Check tag interpolation with a built-in type: list.
+
+ """
+ item = [[1, 2, 3]]
+ attr_name = 'pop'
+ # Make a copy to prevent changes to item[0].
+ self._assert_builtin_attr(list(item[0]), attr_name, 3)
+
+ template = '{{#section}}{{%s}}{{/section}}' % attr_name
+ context = {'section': item, attr_name: 7}
+ self._assert_render(u'7', template, context)
+
+ def test_implicit_iterator__literal(self):
+ """
+ Test an implicit iterator in a literal tag.
+
+ """
+ template = """{{#test}}{{{.}}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'<>', template, context)
+
+ def test_implicit_iterator__escaped(self):
+ """
+ Test an implicit iterator in a normal tag.
+
+ """
+ template = """{{#test}}{{.}}{{/test}}"""
+ context = {'test': ['<', '>']}
+
+ self._assert_render(u'&lt;&gt;', template, context)
+
+ def test_literal__in_section(self):
+ """
+ Check that literals work in sections.
+
+ """
+ template = '{{#test}}1 {{{less_than}}} 2{{/test}}'
+ context = {'test': {'less_than': '<'}}
+
+ self._assert_render(u'1 < 2', template, context)
+
+ def test_literal__in_partial(self):
+ """
+ Check that literals work in partials.
+
+ """
+ template = '{{>partial}}'
+ partials = {'partial': '1 {{{less_than}}} 2'}
+ context = {'less_than': '<'}
+
+ self._assert_render(u'1 < 2', template, context, partials=partials)
+
+ def test_partial(self):
+ partials = {'partial': "{{person}}"}
+ self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, partials=partials)
+
+ def test_partial__context_values(self):
+ """
+ Test that escape and literal work on context values in partials.
+
+ """
+ engine = self._engine()
+
+ template = '{{>partial}}'
+ partials = {'partial': 'unescaped: {{{foo}}} escaped: {{foo}}'}
+ context = {'foo': '<'}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+
+ ## Test cases related specifically to sections.
+
+ def test_section__end_tag_with_no_start_tag(self):
+ """
+ Check what happens if there is an end tag with no start tag.
+
+ """
+ template = '{{/section}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEqual(str(err), "Section end tag mismatch: section != None")
+
+ def test_section__end_tag_mismatch(self):
+ """
+ Check what happens if the end tag doesn't match.
+
+ """
+ template = '{{#section_start}}{{/section_end}}'
+ try:
+ self._assert_render(None, template)
+ except ParsingError, err:
+ self.assertEqual(str(err), "Section end tag mismatch: section_end != section_start")
+
+ def test_section__context_values(self):
+ """
+ Test that escape and literal work on context values in sections.
+
+ """
+ engine = self._engine()
+
+ template = '{{#test}}unescaped: {{{foo}}} escaped: {{foo}}{{/test}}'
+ context = {'test': {'foo': '<'}}
+
+ self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine)
+
+ def test_section__context_precedence(self):
+ """
+ Check that items higher in the context stack take precedence.
+
+ """
+ template = '{{entree}} : {{#vegetarian}}{{entree}}{{/vegetarian}}'
+ context = {'entree': 'chicken', 'vegetarian': {'entree': 'beans and rice'}}
+ self._assert_render(u'chicken : beans and rice', template, context)
+
+ def test_section__list_referencing_outer_context(self):
+ """
+ Check that list items can access the parent context.
+
+ For sections whose value is a list, check that items in the list
+ have access to the values inherited from the parent context
+ when rendering.
+
+ """
+ context = {
+ "greeting": "Hi",
+ "list": [{"name": "Al"}, {"name": "Bob"}],
+ }
+
+ template = "{{#list}}{{greeting}} {{name}}, {{/list}}"
+
+ self._assert_render(u"Hi Al, Hi Bob, ", template, context)
+
+ def test_section__output_not_interpolated(self):
+ """
+ Check that rendered section output is not interpolated.
+
+ """
+ template = '{{#section}}{{template}}{{/section}}: {{planet}}'
+ context = {'section': True, 'template': '{{planet}}', 'planet': 'Earth'}
+ self._assert_render(u'{{planet}}: Earth', template, context)
+
+ # TODO: have this test case added to the spec.
+ def test_section__string_values_not_lists(self):
+ """
+ Check that string section values are not interpreted as lists.
+
+ """
+ template = '{{#section}}foo{{/section}}'
+ context = {'section': '123'}
+ # If strings were interpreted as lists, this would give "foofoofoo".
+ self._assert_render(u'foo', template, context)
+
+ def test_section__nested_truthy(self):
+ """
+ Check that "nested truthy" sections get rendered.
+
+ Test case for issue #24: https://github.com/defunkt/pystache/issues/24
+
+ This test is copied from the spec. We explicitly include it to
+ prevent regressions for those who don't pull down the spec tests.
+
+ """
+ template = '| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |'
+ context = {'bool': True}
+ self._assert_render(u'| A B C D E |', template, context)
+
+ def test_section__nested_with_same_keys(self):
+ """
+ Check a doubly-nested section with the same context key.
+
+ Test case for issue #36: https://github.com/defunkt/pystache/issues/36
+
+ """
+ # Start with an easier, working case.
+ template = '{{#x}}{{#z}}{{y}}{{/z}}{{/x}}'
+ context = {'x': {'z': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ template = '{{#x}}{{#x}}{{y}}{{/x}}{{/x}}'
+ context = {'x': {'x': {'y': 1}}}
+ self._assert_render(u'1', template, context)
+
+ def test_section__lambda(self):
+ template = '{{#test}}Mom{{/test}}'
+ context = {'test': (lambda text: 'Hi %s' % text)}
+ self._assert_render(u'Hi Mom', template, context)
+
+ def test_section__iterable(self):
+ """
+ Check that objects supporting iteration (aside from dicts) behave like lists.
+
+ """
+ template = '{{#iterable}}{{.}}{{/iterable}}'
+
+ context = {'iterable': (i for i in range(3))} # type 'generator'
+ self._assert_render(u'012', template, context)
+
+ context = {'iterable': xrange(4)} # type 'xrange'
+ self._assert_render(u'0123', template, context)
+
+ d = {'foo': 0, 'bar': 0}
+ # We don't know what order of keys we'll be given, but from the
+ # Python documentation:
+ # "If items(), keys(), values(), iteritems(), iterkeys(), and
+ # itervalues() are called with no intervening modifications to
+ # the dictionary, the lists will directly correspond."
+ expected = u''.join(d.keys())
+ context = {'iterable': d.iterkeys()} # type 'dictionary-keyiterator'
+ self._assert_render(expected, template, context)
+
+ def test_section__lambda__tag_in_output(self):
+ """
+ Check that callable output is treated as a template string (issue #46).
+
+ The spec says--
+
+ When used as the data value for a Section tag, the lambda MUST
+ be treatable as an arity 1 function, and invoked as such (passing
+ a String containing the unprocessed section contents). The
+ returned value MUST be rendered against the current delimiters,
+ then interpolated in place of the section.
+
+ """
+ template = '{{#test}}Hi {{person}}{{/test}}'
+ context = {'person': 'Mom', 'test': (lambda text: text + " :)")}
+ self._assert_render(u'Hi Mom :)', template, context)
+
+ def test_section__lambda__not_on_context_stack(self):
+ """
+ Check that section lambdas are not pushed onto the context stack.
+
+ Even though the sections spec says that section data values should be
+ pushed onto the context stack prior to rendering, this does not apply
+ to lambdas. Lambdas obey their own special case.
+
+ This test case is equivalent to a test submitted to the Mustache spec here:
+
+ https://github.com/mustache/spec/pull/47 .
+
+ """
+ context = {'foo': 'bar', 'lambda': (lambda text: "{{.}}")}
+ template = '{{#foo}}{{#lambda}}blah{{/lambda}}{{/foo}}'
+ self._assert_render(u'bar', template, context)
+
+ def test_section__lambda__no_reinterpolation(self):
+ """
+ Check that section lambda return values are not re-interpolated.
+
+ This test is a sanity check that the rendered lambda return value
+ is not re-interpolated as could be construed by reading the
+ section part of the Mustache spec.
+
+ This test case is equivalent to a test submitted to the Mustache spec here:
+
+ https://github.com/mustache/spec/pull/47 .
+
+ """
+ template = '{{#planet}}{{#lambda}}dot{{/lambda}}{{/planet}}'
+ context = {'planet': 'Earth', 'dot': '~{{.}}~', 'lambda': (lambda text: "#{{%s}}#" % text)}
+ self._assert_render(u'#~{{.}}~#', template, context)
+
+ def test_comment__multiline(self):
+ """
+ Check that multiline comments are permitted.
+
+ """
+ self._assert_render(u'foobar', 'foo{{! baz }}bar')
+ self._assert_render(u'foobar', 'foo{{! \nbaz }}bar')
+
+ def test_custom_delimiters__sections(self):
+ """
+ Check that custom delimiters can be used to start a section.
+
+ Test case for issue #20: https://github.com/defunkt/pystache/issues/20
+
+ """
+ template = '{{=[[ ]]=}}[[#foo]]bar[[/foo]]'
+ context = {'foo': True}
+ self._assert_render(u'bar', template, context)
+
+ def test_custom_delimiters__not_retroactive(self):
+ """
+ Check that changing custom delimiters back is not "retroactive."
+
+ Test case for issue #35: https://github.com/defunkt/pystache/issues/35
+
+ """
+ expected = u' {{foo}} '
+ self._assert_render(expected, '{{=$ $=}} {{foo}} ')
+ self._assert_render(expected, '{{=$ $=}} {{foo}} $={{ }}=$') # was yielding u' '.
+
+ def test_dot_notation(self):
+ """
+ Check that we can use dot notation when the variable is a dict,
+ a used-defined object, or a combination of both
+ """
+ template = 'Hello, {{person.name}}. I see you are {{person.details.age}}.'
+ person = Attachable(name='Biggles', details={'age': 42})
+ context = {'person': person}
+ self._assert_render(u'Hello, Biggles. I see you are 42.', template, context)
+
+ def test_dot_notation__missing_attributes_or_keys(self):
+ """
+ Check that, when using dot notation, if the key or attribute does not
+ exist then its value is rendered as empty
+ """
+ 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):
+ """
+ Check that using multiple levels of dot attributes works as expected
+ """
+ template = """Hello, Mr. {{person.name.lastname}}.
+ I see you're back from {{person.travels.last.country.city}}.
+ I'm missing some of your details: {{person.details.private.editor}}."""
+ expected = u"""Hello, Mr. Pither.
+ I see you're back from Cornwall.
+ I'm missing some of your details: ."""
+ context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
+ 'travels': {'last': {'country': {'city': 'Cornwall'}}},
+ 'details': {'public': 'likes cycling'}}}
+ self._assert_render(expected, template, context)
+
+ # It should also work with user-defined objects
+ context = {'person': Attachable(name={'firstname': 'unknown', 'lastname': 'Pither'},
+ travels=Attachable(last=Attachable(country=Attachable(city='Cornwall'))),
+ details=Attachable())}
+ self._assert_render(expected, template, context)
diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py
new file mode 100644
index 0000000..64a4325
--- /dev/null
+++ b/pystache/tests/test_renderer.py
@@ -0,0 +1,584 @@
+# coding: utf-8
+
+"""
+Unit tests of template.py.
+
+"""
+
+import codecs
+import os
+import sys
+import unittest
+
+from examples.simple import Simple
+from pystache import Renderer
+from pystache import TemplateSpec
+from pystache.loader import Loader
+
+from pystache.tests.common import get_data_path, AssertStringMixin
+from pystache.tests.data.views import SayHello
+
+
+def _make_renderer():
+ """
+ Return a default Renderer instance for testing purposes.
+
+ """
+ renderer = Renderer(string_encoding='ascii', file_encoding='ascii')
+ return renderer
+
+
+def mock_unicode(b, encoding=None):
+ if encoding is None:
+ encoding = 'ascii'
+ u = unicode(b, encoding=encoding)
+ return u.upper()
+
+
+class RendererInitTestCase(unittest.TestCase):
+
+ """
+ Tests the Renderer.__init__() method.
+
+ """
+
+ def test_partials__default(self):
+ """
+ Test the default value.
+
+ """
+ renderer = Renderer()
+ self.assertTrue(renderer.partials is None)
+
+ def test_partials(self):
+ """
+ Test that the attribute is set correctly.
+
+ """
+ renderer = Renderer(partials={'foo': 'bar'})
+ self.assertEqual(renderer.partials, {'foo': 'bar'})
+
+ def test_escape__default(self):
+ escape = Renderer().escape
+
+ self.assertEqual(escape(">"), "&gt;")
+ self.assertEqual(escape('"'), "&quot;")
+ # Single quotes are escaped only in Python 3.2 and later.
+ if sys.version_info < (3, 2):
+ expected = "'"
+ else:
+ expected = '&#x27;'
+ self.assertEqual(escape("'"), expected)
+
+ def test_escape(self):
+ escape = lambda s: "**" + s
+ renderer = Renderer(escape=escape)
+ self.assertEqual(renderer.escape("bar"), "**bar")
+
+ def test_decode_errors__default(self):
+ """
+ Check the default value.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.decode_errors, 'strict')
+
+ def test_decode_errors(self):
+ """
+ Check that the constructor sets the attribute correctly.
+
+ """
+ renderer = Renderer(decode_errors="foo")
+ self.assertEqual(renderer.decode_errors, "foo")
+
+ def test_file_encoding__default(self):
+ """
+ Check the file_encoding default.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.file_encoding, renderer.string_encoding)
+
+ def test_file_encoding(self):
+ """
+ Check that the file_encoding attribute is set correctly.
+
+ """
+ renderer = Renderer(file_encoding='foo')
+ self.assertEqual(renderer.file_encoding, 'foo')
+
+ def test_file_extension__default(self):
+ """
+ Check the file_extension default.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.file_extension, 'mustache')
+
+ def test_file_extension(self):
+ """
+ Check that the file_encoding attribute is set correctly.
+
+ """
+ renderer = Renderer(file_extension='foo')
+ self.assertEqual(renderer.file_extension, 'foo')
+
+ def test_search_dirs__default(self):
+ """
+ Check the search_dirs default.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.search_dirs, [os.curdir])
+
+ def test_search_dirs__string(self):
+ """
+ Check that the search_dirs attribute is set correctly when a string.
+
+ """
+ renderer = Renderer(search_dirs='foo')
+ self.assertEqual(renderer.search_dirs, ['foo'])
+
+ def test_search_dirs__list(self):
+ """
+ Check that the search_dirs attribute is set correctly when a list.
+
+ """
+ renderer = Renderer(search_dirs=['foo'])
+ self.assertEqual(renderer.search_dirs, ['foo'])
+
+ def test_string_encoding__default(self):
+ """
+ Check the default value.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.string_encoding, sys.getdefaultencoding())
+
+ def test_string_encoding(self):
+ """
+ Check that the constructor sets the attribute correctly.
+
+ """
+ renderer = Renderer(string_encoding="foo")
+ self.assertEqual(renderer.string_encoding, "foo")
+
+
+class RendererTests(unittest.TestCase, AssertStringMixin):
+
+ """Test the Renderer class."""
+
+ def _renderer(self):
+ return Renderer()
+
+ ## Test Renderer.unicode().
+
+ def test_unicode__string_encoding(self):
+ """
+ Test that the string_encoding attribute is respected.
+
+ """
+ renderer = self._renderer()
+ b = u"é".encode('utf-8')
+
+ renderer.string_encoding = "ascii"
+ self.assertRaises(UnicodeDecodeError, renderer.unicode, b)
+
+ renderer.string_encoding = "utf-8"
+ self.assertEqual(renderer.unicode(b), u"é")
+
+ def test_unicode__decode_errors(self):
+ """
+ Test that the decode_errors attribute is respected.
+
+ """
+ renderer = self._renderer()
+ renderer.string_encoding = "ascii"
+ b = u"déf".encode('utf-8')
+
+ renderer.decode_errors = "ignore"
+ self.assertEqual(renderer.unicode(b), "df")
+
+ renderer.decode_errors = "replace"
+ # U+FFFD is the official Unicode replacement character.
+ self.assertEqual(renderer.unicode(b), u'd\ufffd\ufffdf')
+
+ ## Test the _make_loader() method.
+
+ def test__make_loader__return_type(self):
+ """
+ Test that _make_loader() returns a Loader.
+
+ """
+ renderer = self._renderer()
+ loader = renderer._make_loader()
+
+ self.assertEqual(type(loader), Loader)
+
+ def test__make_loader__attributes(self):
+ """
+ Test that _make_loader() sets all attributes correctly..
+
+ """
+ unicode_ = lambda x: x
+
+ renderer = self._renderer()
+ renderer.file_encoding = 'enc'
+ renderer.file_extension = 'ext'
+ renderer.unicode = unicode_
+
+ loader = renderer._make_loader()
+
+ self.assertEqual(loader.extension, 'ext')
+ self.assertEqual(loader.file_encoding, 'enc')
+ self.assertEqual(loader.to_unicode, unicode_)
+
+ ## Test the render() method.
+
+ def test_render__return_type(self):
+ """
+ Check that render() returns a string of type unicode.
+
+ """
+ renderer = self._renderer()
+ rendered = renderer.render('foo')
+ self.assertEqual(type(rendered), unicode)
+
+ def test_render__unicode(self):
+ renderer = self._renderer()
+ actual = renderer.render(u'foo')
+ self.assertEqual(actual, u'foo')
+
+ def test_render__str(self):
+ renderer = self._renderer()
+ actual = renderer.render('foo')
+ self.assertEqual(actual, 'foo')
+
+ def test_render__non_ascii_character(self):
+ renderer = self._renderer()
+ actual = renderer.render(u'Poincaré')
+ self.assertEqual(actual, u'Poincaré')
+
+ def test_render__context(self):
+ """
+ Test render(): passing a context.
+
+ """
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}), 'Hi Mom')
+
+ def test_render__context_and_kwargs(self):
+ """
+ Test render(): passing a context and **kwargs.
+
+ """
+ renderer = self._renderer()
+ template = 'Hi {{person1}} and {{person2}}'
+ self.assertEqual(renderer.render(template, {'person1': 'Mom'}, person2='Dad'), 'Hi Mom and Dad')
+
+ def test_render__kwargs_and_no_context(self):
+ """
+ Test render(): passing **kwargs and no context.
+
+ """
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', person='Mom'), 'Hi Mom')
+
+ def test_render__context_and_kwargs__precedence(self):
+ """
+ Test render(): **kwargs takes precedence over context.
+
+ """
+ renderer = self._renderer()
+ self.assertEqual(renderer.render('Hi {{person}}', {'person': 'Mom'}, person='Dad'), 'Hi Dad')
+
+ def test_render__kwargs_does_not_modify_context(self):
+ """
+ Test render(): passing **kwargs does not modify the passed context.
+
+ """
+ context = {}
+ renderer = self._renderer()
+ renderer.render('Hi {{person}}', context=context, foo="bar")
+ self.assertEqual(context, {})
+
+ def test_render__nonascii_template(self):
+ """
+ Test passing a non-unicode template with non-ascii characters.
+
+ """
+ renderer = _make_renderer()
+ template = u"déf".encode("utf-8")
+
+ # Check that decode_errors and string_encoding are both respected.
+ renderer.decode_errors = 'ignore'
+ renderer.string_encoding = 'ascii'
+ self.assertEqual(renderer.render(template), "df")
+
+ renderer.string_encoding = 'utf_8'
+ self.assertEqual(renderer.render(template), u"déf")
+
+ def test_make_load_partial(self):
+ """
+ Test the _make_load_partial() method.
+
+ """
+ renderer = Renderer()
+ renderer.partials = {'foo': 'bar'}
+ load_partial = renderer._make_load_partial()
+
+ actual = load_partial('foo')
+ self.assertEqual(actual, 'bar')
+ self.assertEqual(type(actual), unicode, "RenderEngine requires that "
+ "load_partial return unicode strings.")
+
+ def test_make_load_partial__unicode(self):
+ """
+ Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
+
+ """
+ renderer = Renderer()
+
+ renderer.partials = {'partial': 'foo'}
+ load_partial = renderer._make_load_partial()
+ self.assertEqual(load_partial("partial"), "foo")
+
+ # Now with a value that is already unicode.
+ renderer.partials = {'partial': u'foo'}
+ load_partial = renderer._make_load_partial()
+ # If the next line failed, we would get the following error:
+ # TypeError: decoding Unicode is not supported
+ self.assertEqual(load_partial("partial"), "foo")
+
+ def test_render_path(self):
+ """
+ Test the render_path() method.
+
+ """
+ renderer = Renderer()
+ path = get_data_path('say_hello.mustache')
+ actual = renderer.render_path(path, to='foo')
+ self.assertEqual(actual, "Hello, foo")
+
+ def test_render__object(self):
+ """
+ Test rendering an object instance.
+
+ """
+ renderer = Renderer()
+
+ say_hello = SayHello()
+ actual = renderer.render(say_hello)
+ self.assertEqual('Hello, World', actual)
+
+ actual = renderer.render(say_hello, to='Mars')
+ self.assertEqual('Hello, Mars', actual)
+
+ def test_render__template_spec(self):
+ """
+ Test rendering a TemplateSpec instance.
+
+ """
+ renderer = Renderer()
+
+ class Spec(TemplateSpec):
+ template = "hello, {{to}}"
+ to = 'world'
+
+ spec = Spec()
+ actual = renderer.render(spec)
+ self.assertString(actual, u'hello, world')
+
+ def test_render__view(self):
+ """
+ Test rendering a View instance.
+
+ """
+ renderer = Renderer()
+
+ view = Simple()
+ actual = renderer.render(view)
+ self.assertEqual('Hi pizza!', actual)
+
+
+# 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):
+
+ """
+ Check the RenderEngine returned by Renderer._make_render_engine().
+
+ """
+
+ def _make_renderer(self):
+ """
+ Return a default Renderer instance for testing purposes.
+
+ """
+ return _make_renderer()
+
+ ## Test the engine's load_partial attribute.
+
+ def test__load_partial__returns_unicode(self):
+ """
+ Check that load_partial returns unicode (and not a subclass).
+
+ """
+ class MyUnicode(unicode):
+ pass
+
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+ renderer.partials = {'str': 'foo', 'subclass': MyUnicode('abc')}
+
+ engine = renderer._make_render_engine()
+
+ actual = engine.load_partial('str')
+ self.assertEqual(actual, "foo")
+ self.assertEqual(type(actual), unicode)
+
+ # Check that unicode subclasses are not preserved.
+ actual = engine.load_partial('subclass')
+ self.assertEqual(actual, "abc")
+ self.assertEqual(type(actual), unicode)
+
+ def test__load_partial__not_found(self):
+ """
+ Check that load_partial provides a nice message when a template is not found.
+
+ """
+ renderer = Renderer()
+ renderer.partials = {}
+
+ engine = renderer._make_render_engine()
+ load_partial = engine.load_partial
+
+ try:
+ load_partial("foo")
+ raise Exception("Shouldn't get here")
+ except Exception, err:
+ self.assertEqual(str(err), "Partial not found with name: 'foo'")
+
+ ## Test the engine's literal attribute.
+
+ def test__literal__uses_renderer_unicode(self):
+ """
+ Test that literal uses the renderer's unicode function.
+
+ """
+ renderer = self._make_renderer()
+ renderer.unicode = mock_unicode
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ b = u"foo".encode("ascii")
+ self.assertEqual(literal(b), "FOO")
+
+ def test__literal__handles_unicode(self):
+ """
+ Test that literal doesn't try to "double decode" unicode.
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ self.assertEqual(literal(u"foo"), "foo")
+
+ def test__literal__returns_unicode(self):
+ """
+ Test that literal returns unicode (and not a subclass).
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ literal = engine.literal
+
+ self.assertEqual(type(literal("foo")), unicode)
+
+ class MyUnicode(unicode):
+ pass
+
+ s = MyUnicode("abc")
+
+ self.assertEqual(type(s), MyUnicode)
+ self.assertTrue(isinstance(s, unicode))
+ self.assertEqual(type(literal(s)), unicode)
+
+ ## Test the engine's escape attribute.
+
+ def test__escape__uses_renderer_escape(self):
+ """
+ Test that escape uses the renderer's escape function.
+
+ """
+ renderer = Renderer()
+ renderer.escape = lambda s: "**" + s
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ self.assertEqual(escape("foo"), "**foo")
+
+ def test__escape__uses_renderer_unicode(self):
+ """
+ Test that escape uses the renderer's unicode function.
+
+ """
+ renderer = Renderer()
+ renderer.unicode = mock_unicode
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ b = u"foo".encode('ascii')
+ self.assertEqual(escape(b), "FOO")
+
+ def test__escape__has_access_to_original_unicode_subclass(self):
+ """
+ Test that escape receives strings with the unicode subclass intact.
+
+ """
+ renderer = Renderer()
+ renderer.escape = lambda s: unicode(type(s).__name__)
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ class MyUnicode(unicode):
+ pass
+
+ self.assertEqual(escape(u"foo".encode('ascii')), unicode.__name__)
+ self.assertEqual(escape(u"foo"), unicode.__name__)
+ self.assertEqual(escape(MyUnicode("foo")), MyUnicode.__name__)
+
+ def test__escape__returns_unicode(self):
+ """
+ Test that literal returns unicode (and not a subclass).
+
+ """
+ renderer = Renderer()
+ renderer.string_encoding = 'ascii'
+
+ engine = renderer._make_render_engine()
+ escape = engine.escape
+
+ self.assertEqual(type(escape("foo")), unicode)
+
+ # Check that literal doesn't preserve unicode subclasses.
+ class MyUnicode(unicode):
+ pass
+
+ s = MyUnicode("abc")
+
+ self.assertEqual(type(s), MyUnicode)
+ self.assertTrue(isinstance(s, unicode))
+ self.assertEqual(type(escape(s)), unicode)
+
diff --git a/pystache/tests/test_simple.py b/pystache/tests/test_simple.py
new file mode 100644
index 0000000..d3ed0b6
--- /dev/null
+++ b/pystache/tests/test_simple.py
@@ -0,0 +1,82 @@
+import unittest
+
+import pystache
+from pystache import Renderer
+from examples.nested_context import NestedContext
+from examples.complex import Complex
+from examples.lambdas import Lambdas
+from examples.template_partial import TemplatePartial
+from examples.simple import Simple
+
+from pystache.tests.common import EXAMPLES_DIR
+from pystache.tests.common import AssertStringMixin
+
+
+class TestSimple(unittest.TestCase, AssertStringMixin):
+
+ def test_nested_context(self):
+ renderer = Renderer()
+ view = NestedContext(renderer)
+ view.template = '{{#foo}}{{thing1}} and {{thing2}} and {{outer_thing}}{{/foo}}{{^foo}}Not foo!{{/foo}}'
+
+ actual = renderer.render(view)
+ self.assertString(actual, u"one and foo and two")
+
+ def test_looping_and_negation_context(self):
+ template = '{{#item}}{{header}}: {{name}} {{/item}}{{^item}} Shouldnt see me{{/item}}'
+ context = Complex()
+
+ renderer = Renderer()
+ actual = renderer.render(template, context)
+ self.assertEqual(actual, "Colors: red Colors: green Colors: blue ")
+
+ def test_empty_context(self):
+ template = '{{#empty_list}}Shouldnt see me {{/empty_list}}{{^empty_list}}Should see me{{/empty_list}}'
+ self.assertEqual(pystache.Renderer().render(template), "Should see me")
+
+ def test_callables(self):
+ view = Lambdas()
+ view.template = '{{#replace_foo_with_bar}}foo != bar. oh, it does!{{/replace_foo_with_bar}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'bar != bar. oh, it does!')
+
+ def test_rendering_partial(self):
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+
+ view = TemplatePartial(renderer=renderer)
+ view.template = '{{>inner_partial}}'
+
+ actual = renderer.render(view)
+ self.assertString(actual, u'Again, Welcome!')
+
+ view.template = '{{#looping}}{{>inner_partial}} {{/looping}}'
+ actual = renderer.render(view)
+ self.assertString(actual, u"Again, Welcome! Again, Welcome! Again, Welcome! ")
+
+ def test_non_existent_value_renders_blank(self):
+ view = Simple()
+ template = '{{not_set}} {{blank}}'
+ self.assertEqual(pystache.Renderer().render(template), ' ')
+
+ def test_template_partial_extension(self):
+ """
+ Side note:
+
+ From the spec--
+
+ Partial tags SHOULD be treated as standalone when appropriate.
+
+ In particular, this means that trailing newlines should be removed.
+
+ """
+ renderer = Renderer(search_dirs=EXAMPLES_DIR, file_extension='txt')
+
+ view = TemplatePartial(renderer=renderer)
+
+ actual = renderer.render(view)
+ self.assertString(actual, u"""Welcome
+-------
+
+## Again, Welcome! ##""")
diff --git a/pystache/tests/test_specloader.py b/pystache/tests/test_specloader.py
new file mode 100644
index 0000000..8332b28
--- /dev/null
+++ b/pystache/tests/test_specloader.py
@@ -0,0 +1,423 @@
+# coding: utf-8
+
+"""
+Unit tests for template_spec.py.
+
+"""
+
+import os.path
+import sys
+import unittest
+
+import examples
+from examples.simple import Simple
+from examples.complex import Complex
+from examples.lambdas import Lambdas
+from examples.inverted import Inverted, InvertedLists
+from pystache import Renderer
+from pystache import TemplateSpec
+from pystache.locator import Locator
+from pystache.loader import Loader
+from pystache.specloader import SpecLoader
+from pystache.tests.common import DATA_DIR, EXAMPLES_DIR
+from pystache.tests.common import AssertIsMixin, AssertStringMixin
+from pystache.tests.data.views import SampleView
+from pystache.tests.data.views import NonAscii
+
+
+class Thing(object):
+ pass
+
+
+class ViewTestCase(unittest.TestCase, AssertStringMixin):
+
+ def test_template_rel_directory(self):
+ """
+ Test that View.template_rel_directory is respected.
+
+ """
+ class Tagless(TemplateSpec):
+ pass
+
+ view = Tagless()
+ renderer = Renderer()
+
+ self.assertRaises(IOError, renderer.render, view)
+
+ # TODO: change this test to remove the following brittle line.
+ view.template_rel_directory = "examples"
+ actual = renderer.render(view)
+ self.assertEqual(actual, "No tags...")
+
+ def test_template_path_for_partials(self):
+ """
+ Test that View.template_rel_path is respected for partials.
+
+ """
+ spec = TemplateSpec()
+ spec.template = "Partial: {{>tagless}}"
+
+ renderer1 = Renderer()
+ renderer2 = Renderer(search_dirs=EXAMPLES_DIR)
+
+ self.assertRaises(IOError, renderer1.render, spec)
+
+ actual = renderer2.render(spec)
+ self.assertEqual(actual, "Partial: No tags...")
+
+ def test_basic_method_calls(self):
+ renderer = Renderer()
+ actual = renderer.render(Simple())
+
+ self.assertString(actual, u"Hi pizza!")
+
+ def test_non_callable_attributes(self):
+ view = Simple()
+ view.thing = 'Chris'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertEqual(actual, "Hi Chris!")
+
+ def test_complex(self):
+ renderer = Renderer()
+ actual = renderer.render(Complex())
+ self.assertString(actual, u"""\
+<h1>Colors</h1>
+<ul>
+<li><strong>red</strong></li>
+<li><a href="#Green">green</a></li>
+<li><a href="#Blue">blue</a></li>
+</ul>""")
+
+ def test_higher_order_replace(self):
+ renderer = Renderer()
+ actual = renderer.render(Lambdas())
+ self.assertEqual(actual, 'bar != bar. oh, it does!')
+
+ def test_higher_order_rot13(self):
+ view = Lambdas()
+ view.template = '{{#rot13}}abcdefghijklm{{/rot13}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'nopqrstuvwxyz')
+
+ def test_higher_order_lambda(self):
+ view = Lambdas()
+ view.template = '{{#sort}}zyxwvutsrqponmlkjihgfedcba{{/sort}}'
+
+ renderer = Renderer()
+ actual = renderer.render(view)
+ self.assertString(actual, u'abcdefghijklmnopqrstuvwxyz')
+
+ def test_partials_with_lambda(self):
+ view = Lambdas()
+ view.template = '{{>partial_with_lambda}}'
+
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(view)
+ self.assertEqual(actual, u'nopqrstuvwxyz')
+
+ def test_hierarchical_partials_with_lambdas(self):
+ view = Lambdas()
+ view.template = '{{>partial_with_partial_and_lambda}}'
+
+ renderer = Renderer(search_dirs=EXAMPLES_DIR)
+ actual = renderer.render(view)
+ self.assertString(actual, u'nopqrstuvwxyznopqrstuvwxyz')
+
+ def test_inverted(self):
+ renderer = Renderer()
+ actual = renderer.render(Inverted())
+ self.assertString(actual, u"""one, two, three, empty list""")
+
+ def test_accessing_properties_on_parent_object_from_child_objects(self):
+ parent = Thing()
+ parent.this = 'derp'
+ parent.children = [Thing()]
+ view = Simple()
+ view.template = "{{#parent}}{{#children}}{{this}}{{/children}}{{/parent}}"
+
+ renderer = Renderer()
+ actual = renderer.render(view, {'parent': parent})
+
+ self.assertString(actual, u'derp')
+
+ def test_inverted_lists(self):
+ renderer = Renderer()
+ actual = renderer.render(InvertedLists())
+ self.assertString(actual, u"""one, two, three, empty list""")
+
+
+def _make_specloader():
+ """
+ Return a default SpecLoader instance for testing purposes.
+
+ """
+ # Python 2 and 3 have different default encodings. Thus, to have
+ # consistent test results across both versions, we need to specify
+ # the string and file encodings explicitly rather than relying on
+ # the defaults.
+ def to_unicode(s, encoding=None):
+ """
+ Raises a TypeError exception if the given string is already unicode.
+
+ """
+ if encoding is None:
+ encoding = 'ascii'
+ return unicode(s, encoding, 'strict')
+
+ loader = Loader(file_encoding='ascii', to_unicode=to_unicode)
+ return SpecLoader(loader=loader)
+
+
+class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+
+ """
+ Tests template_spec.SpecLoader.
+
+ """
+
+ def _make_specloader(self):
+ return _make_specloader()
+
+ def test_init__defaults(self):
+ spec_loader = SpecLoader()
+
+ # Check the loader attribute.
+ loader = spec_loader.loader
+ self.assertEqual(loader.extension, 'mustache')
+ self.assertEqual(loader.file_encoding, sys.getdefaultencoding())
+ # TODO: finish testing the other Loader attributes.
+ to_unicode = loader.to_unicode
+
+ def test_init__loader(self):
+ loader = Loader()
+ custom = SpecLoader(loader=loader)
+
+ self.assertIs(custom.loader, loader)
+
+ # TODO: rename to something like _assert_load().
+ def _assert_template(self, loader, custom, expected):
+ self.assertString(loader.load(custom), expected)
+
+ def test_load__template__type_str(self):
+ """
+ Test the template attribute: str string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = "abc"
+
+ spec_loader = self._make_specloader()
+ self._assert_template(spec_loader, custom, u"abc")
+
+ def test_load__template__type_unicode(self):
+ """
+ Test the template attribute: unicode string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u"abc"
+
+ spec_loader = self._make_specloader()
+ self._assert_template(spec_loader, custom, u"abc")
+
+ def test_load__template__unicode_non_ascii(self):
+ """
+ Test the template attribute: non-ascii unicode string.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u"é"
+
+ spec_loader = self._make_specloader()
+ self._assert_template(spec_loader, custom, u"é")
+
+ def test_load__template__with_template_encoding(self):
+ """
+ Test the template attribute: with template encoding attribute.
+
+ """
+ custom = TemplateSpec()
+ custom.template = u'é'.encode('utf-8')
+
+ spec_loader = self._make_specloader()
+
+ self.assertRaises(UnicodeDecodeError, self._assert_template, spec_loader, custom, u'é')
+
+ custom.template_encoding = 'utf-8'
+ self._assert_template(spec_loader, custom, u'é')
+
+ # TODO: make this test complete.
+ def test_load__template__correct_loader(self):
+ """
+ Test that reader.unicode() is called correctly.
+
+ This test tests that the correct reader is called with the correct
+ arguments. This is a catch-all test to supplement the other
+ test cases. It tests SpecLoader.load() independent of reader.unicode()
+ being implemented correctly (and tested).
+
+ """
+ class MockLoader(Loader):
+
+ def __init__(self):
+ self.s = None
+ self.encoding = None
+
+ # Overrides the existing method.
+ def unicode(self, s, encoding=None):
+ self.s = s
+ self.encoding = encoding
+ return u"foo"
+
+ loader = MockLoader()
+ custom_loader = SpecLoader()
+ custom_loader.loader = loader
+
+ view = TemplateSpec()
+ view.template = "template-foo"
+ view.template_encoding = "encoding-foo"
+
+ # Check that our unicode() above was called.
+ self._assert_template(custom_loader, view, u'foo')
+ self.assertEqual(loader.s, "template-foo")
+ self.assertEqual(loader.encoding, "encoding-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):
+
+ def _make_loader(self):
+ return _make_specloader()
+
+ def _assert_template_location(self, view, expected):
+ loader = self._make_loader()
+ actual = loader._find_relative(view)
+ self.assertEqual(actual, expected)
+
+ def test_find_relative(self):
+ """
+ Test _find_relative(): default behavior (no attributes set).
+
+ """
+ view = SampleView()
+ self._assert_template_location(view, (None, 'sample_view.mustache'))
+
+ def test_find_relative__template_rel_path__file_name_only(self):
+ """
+ Test _find_relative(): template_rel_path attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_path = 'template.txt'
+ self._assert_template_location(view, ('', 'template.txt'))
+
+ def test_find_relative__template_rel_path__file_name_with_directory(self):
+ """
+ Test _find_relative(): template_rel_path attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_path = 'foo/bar/template.txt'
+ self._assert_template_location(view, ('foo/bar', 'template.txt'))
+
+ def test_find_relative__template_rel_directory(self):
+ """
+ Test _find_relative(): template_rel_directory attribute.
+
+ """
+ view = SampleView()
+ view.template_rel_directory = 'foo'
+
+ self._assert_template_location(view, ('foo', 'sample_view.mustache'))
+
+ def test_find_relative__template_name(self):
+ """
+ Test _find_relative(): template_name attribute.
+
+ """
+ view = SampleView()
+ view.template_name = 'new_name'
+ self._assert_template_location(view, (None, 'new_name.mustache'))
+
+ def test_find_relative__template_extension(self):
+ """
+ Test _find_relative(): template_extension attribute.
+
+ """
+ view = SampleView()
+ 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.
+
+ """
+ loader = self._make_loader()
+
+ view = SampleView()
+ view.template_rel_path = 'foo/bar.txt'
+ self.assertTrue(loader._find_relative(view)[0] is not None)
+
+ actual = loader._find(view)
+ expected = os.path.join(DATA_DIR, 'foo/bar.txt')
+
+ self._assert_paths(actual, expected)
+
+ def test_find__without_directory(self):
+ """
+ Test _find() with a view that doesn't have a directory specified.
+
+ """
+ loader = self._make_loader()
+
+ view = SampleView()
+ self.assertTrue(loader._find_relative(view)[0] is None)
+
+ actual = loader._find(view)
+ expected = os.path.join(DATA_DIR, 'sample_view.mustache')
+
+ self._assert_paths(actual, expected)
+
+ def _assert_get_template(self, custom, expected):
+ loader = self._make_loader()
+ actual = loader.load(custom)
+
+ self.assertEqual(type(actual), unicode)
+ self.assertEqual(actual, expected)
+
+ def test_get_template(self):
+ """
+ Test get_template(): default behavior (no attributes set).
+
+ """
+ view = SampleView()
+
+ self._assert_get_template(view, u"ascii: abc")
+
+ def test_get_template__template_encoding(self):
+ """
+ Test get_template(): template_encoding attribute.
+
+ """
+ view = NonAscii()
+
+ self.assertRaises(UnicodeDecodeError, self._assert_get_template, view, 'foo')
+
+ view.template_encoding = 'utf-8'
+ self._assert_get_template(view, u"non-ascii: é")