summaryrefslogtreecommitdiff
path: root/pystache
diff options
context:
space:
mode:
Diffstat (limited to 'pystache')
-rw-r--r--pystache/__init__.py6
-rw-r--r--pystache/common.py35
-rw-r--r--pystache/context.py61
-rw-r--r--pystache/defaults.py8
-rw-r--r--pystache/init.py1
-rw-r--r--pystache/parsed.py42
-rw-r--r--pystache/parser.py333
-rw-r--r--pystache/renderengine.py274
-rw-r--r--pystache/renderer.py219
-rw-r--r--pystache/tests/common.py2
-rw-r--r--pystache/tests/doctesting.py6
-rw-r--r--pystache/tests/main.py93
-rw-r--r--pystache/tests/test___init__.py2
-rw-r--r--pystache/tests/test_context.py65
-rw-r--r--pystache/tests/test_locator.py12
-rw-r--r--pystache/tests/test_parser.py3
-rw-r--r--pystache/tests/test_renderengine.py122
-rw-r--r--pystache/tests/test_renderer.py142
18 files changed, 898 insertions, 528 deletions
diff --git a/pystache/__init__.py b/pystache/__init__.py
index b07eb65..defab52 100644
--- a/pystache/__init__.py
+++ b/pystache/__init__.py
@@ -6,8 +6,8 @@ TODO: add a docstring.
# We keep all initialization code in a separate module.
-from pystache.init import render, Renderer, TemplateSpec
+from pystache.init import parse, render, Renderer, TemplateSpec
-__all__ = ['render', 'Renderer', 'TemplateSpec']
+__all__ = ['parse', 'render', 'Renderer', 'TemplateSpec']
-__version__ = '0.5.2' # Also change in setup.py.
+__version__ = '0.5.3-alpha' # Also change in setup.py.
diff --git a/pystache/common.py b/pystache/common.py
index c1fd7a1..fb266dd 100644
--- a/pystache/common.py
+++ b/pystache/common.py
@@ -5,6 +5,33 @@ Exposes functionality needed throughout the project.
"""
+from sys import version_info
+
+def _get_string_types():
+ # 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 version_info < (3, ):
+ return basestring
+ # The latter evaluates to "bytes" in Python 3 -- even after conversion by 2to3.
+ return (unicode, type(u"a".encode('utf-8')))
+
+
+_STRING_TYPES = _get_string_types()
+
+
+def is_string(obj):
+ """
+ Return whether the given object is a byte string or unicode string.
+
+ This function is provided for compatibility with both Python 2 and 3
+ when using 2to3.
+
+ """
+ return isinstance(obj, _STRING_TYPES)
+
+
# This function was designed to be portable across Python versions -- both
# with older versions and with Python 3 after applying 2to3.
def read(path):
@@ -26,6 +53,14 @@ def read(path):
f.close()
+class MissingTags(object):
+
+ """Contains the valid values for Renderer.missing_tags."""
+
+ ignore = 'ignore'
+ strict = 'strict'
+
+
class PystacheError(Exception):
"""Base class for Pystache exceptions."""
pass
diff --git a/pystache/context.py b/pystache/context.py
index 8a95059..6715916 100644
--- a/pystache/context.py
+++ b/pystache/context.py
@@ -14,6 +14,9 @@ spec, we define these categories mutually exclusively as follows:
"""
+from pystache.common import PystacheError
+
+
# This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
_BUILTIN_MODULE = type(0).__module__
@@ -55,8 +58,15 @@ def _get_value(context, key):
# types like integers and strings as objects (cf. issue #81).
# Instances of user-defined classes on the other hand, for example,
# are considered objects by the test above.
- if hasattr(context, key):
+ try:
attr = getattr(context, key)
+ except AttributeError:
+ # TODO: distinguish the case of the attribute not existing from
+ # an AttributeError being raised by the call to the attribute.
+ # See the following issue for implementation ideas:
+ # http://bugs.python.org/issue7559
+ pass
+ else:
# TODO: consider using EAFP here instead.
# http://docs.python.org/glossary.html#term-eafp
if callable(attr):
@@ -66,6 +76,21 @@ def _get_value(context, key):
return _NOT_FOUND
+class KeyNotFoundError(PystacheError):
+
+ """
+ An exception raised when a key is not found in a context stack.
+
+ """
+
+ def __init__(self, key, details):
+ self.key = key
+ self.details = details
+
+ def __str__(self):
+ return "Key %s not found: %s" % (repr(self.key), self.details)
+
+
class ContextStack(object):
"""
@@ -175,7 +200,7 @@ class ContextStack(object):
# TODO: add more unit tests for this.
# TODO: update the docstring for dotted names.
- def get(self, name, default=u''):
+ def get(self, name):
"""
Resolve a dotted name against the current context stack.
@@ -245,18 +270,19 @@ class ContextStack(object):
"""
if name == '.':
- # TODO: should we add a test case for an empty context stack?
- return self.top()
+ try:
+ return self.top()
+ except IndexError:
+ raise KeyNotFoundError(".", "empty context stack")
parts = name.split('.')
- result = self._get_simple(parts[0])
+ try:
+ result = self._get_simple(parts[0])
+ except KeyNotFoundError:
+ raise KeyNotFoundError(name, "first part")
for part in parts[1:]:
- # TODO: consider using EAFP here instead.
- # http://docs.python.org/glossary.html#term-eafp
- if result is _NOT_FOUND:
- break
# The full context stack is not used to resolve the remaining parts.
# From the spec--
#
@@ -268,9 +294,10 @@ class ContextStack(object):
#
# TODO: make sure we have a test case for the above point.
result = _get_value(result, part)
-
- if result is _NOT_FOUND:
- return default
+ # TODO: consider using EAFP here instead.
+ # http://docs.python.org/glossary.html#term-eafp
+ if result is _NOT_FOUND:
+ raise KeyNotFoundError(name, "missing %s" % repr(part))
return result
@@ -279,16 +306,12 @@ class ContextStack(object):
Query the stack for a non-dotted name.
"""
- result = _NOT_FOUND
-
for item in reversed(self._stack):
result = _get_value(item, name)
- if result is _NOT_FOUND:
- continue
- # Otherwise, the key was found.
- break
+ if result is not _NOT_FOUND:
+ return result
- return result
+ raise KeyNotFoundError(name, "part missing")
def push(self, item):
"""
diff --git a/pystache/defaults.py b/pystache/defaults.py
index fcd04c3..0a20328 100644
--- a/pystache/defaults.py
+++ b/pystache/defaults.py
@@ -17,6 +17,8 @@ except ImportError:
import os
import sys
+from pystache.common import MissingTags
+
# How to handle encoding errors when decoding strings from str to unicode.
#
@@ -36,6 +38,12 @@ STRING_ENCODING = sys.getdefaultencoding()
# strings that arise from files.
FILE_ENCODING = sys.getdefaultencoding()
+# The delimiters to start with when parsing.
+DELIMITERS = (u'{{', u'}}')
+
+# How to handle missing tags when rendering a template.
+MISSING_TAGS = MissingTags.ignore
+
# The starting list of directories in which to search for templates when
# loading a template by file name.
SEARCH_DIRS = [os.curdir] # i.e. ['.']
diff --git a/pystache/init.py b/pystache/init.py
index e9d854d..38bb1f5 100644
--- a/pystache/init.py
+++ b/pystache/init.py
@@ -5,6 +5,7 @@ This module contains the initialization logic called by __init__.py.
"""
+from pystache.parser import parse
from pystache.renderer import Renderer
from pystache.template_spec import TemplateSpec
diff --git a/pystache/parsed.py b/pystache/parsed.py
index a37565b..e94c644 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -3,50 +3,40 @@
"""
Exposes a class that represents a parsed (or compiled) template.
-This module is meant only for internal use.
-
"""
class ParsedTemplate(object):
- def __init__(self, parse_tree):
- """
- Arguments:
+ def __init__(self):
+ self._parse_tree = []
- parse_tree: a list, each element of which is either--
-
- (1) a unicode string, or
- (2) a "rendering" callable that accepts a ContextStack instance
- and returns a unicode string.
+ def __repr__(self):
+ return repr(self._parse_tree)
- The possible rendering callables are the return values of the
- following functions:
+ def add(self, node):
+ """
+ Arguments:
- * RenderEngine._make_get_escaped()
- * RenderEngine._make_get_inverse()
- * RenderEngine._make_get_literal()
- * RenderEngine._make_get_partial()
- * RenderEngine._make_get_section()
+ node: a unicode string or node object instance. A node object
+ instance must have a `render(engine, stack)` method that
+ accepts a RenderEngine instance and a ContextStack instance and
+ returns a unicode string.
"""
- self._parse_tree = parse_tree
+ self._parse_tree.append(node)
- def __repr__(self):
- return "[%s]" % (", ".join([repr(part) for part in self._parse_tree]))
-
- def render(self, context):
+ def render(self, engine, context):
"""
Returns: a string of type unicode.
"""
# We avoid use of the ternary operator for Python 2.4 support.
def get_unicode(val):
- if callable(val):
- return val(context)
- return val
+ if type(val) is unicode:
+ return val
+ return val.render(engine, context)
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
return unicode(s)
-
diff --git a/pystache/parser.py b/pystache/parser.py
index 4e05f3b..4c37ec3 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -1,31 +1,51 @@
# coding: utf-8
"""
-Provides a class for parsing template strings.
-
-This module is only meant for internal use by the renderengine module.
+Exposes a parse() function to parse template strings.
"""
import re
-from pystache.common import TemplateNotFoundError
+from pystache.defaults import DELIMITERS
from pystache.parsed import ParsedTemplate
-DEFAULT_DELIMITERS = (u'{{', u'}}')
END_OF_LINE_CHARACTERS = [u'\r', u'\n']
NON_BLANK_RE = re.compile(ur'^(.)', re.M)
-def _compile_template_re(delimiters=None):
+# TODO: add some unit tests for this.
+# TODO: add a test case that checks for spurious spaces.
+# TODO: add test cases for delimiters.
+def parse(template, delimiters=None):
"""
- Return a regular expresssion object (re.RegexObject) instance.
+ Parse a unicode template string and return a ParsedTemplate instance.
+
+ Arguments:
+
+ template: a unicode template string.
+
+ delimiters: a 2-tuple of delimiters. Defaults to the package default.
+
+ Examples:
+
+ >>> parsed = parse(u"Hey {{#who}}{{name}}!{{/who}}")
+ >>> print str(parsed).replace('u', '') # This is a hack to get the test to pass both in Python 2 and 3.
+ ['Hey ', _SectionNode(key='who', index_begin=12, index_end=21, parsed=[_EscapeNode(key='name'), '!'])]
"""
- if delimiters is None:
- delimiters = DEFAULT_DELIMITERS
+ if type(template) is not unicode:
+ raise Exception("Template is not unicode: %s" % type(template))
+ parser = _Parser(delimiters)
+ return parser.parse(template)
+
+
+def _compile_template_re(delimiters):
+ """
+ Return a regular expresssion object (re.RegexObject) instance.
+ """
# The possible tag type characters following the opening tag,
# excluding "=" and "{".
tag_types = "!>&/#^"
@@ -54,34 +74,172 @@ class ParsingError(Exception):
pass
-class Parser(object):
+## Node types
- _delimiters = None
- _template_re = None
+def _format(obj, exclude=None):
+ if exclude is None:
+ exclude = []
+ exclude.append('key')
+ attrs = obj.__dict__
+ names = list(set(attrs.keys()) - set(exclude))
+ names.sort()
+ names.insert(0, 'key')
+ args = ["%s=%s" % (name, repr(attrs[name])) for name in names]
+ return "%s(%s)" % (obj.__class__.__name__, ", ".join(args))
- def __init__(self, engine, delimiters=None):
- """
- Construct an instance.
- Arguments:
+class _CommentNode(object):
- engine: a RenderEngine instance.
+ def __repr__(self):
+ return _format(self)
- """
+ def render(self, engine, context):
+ return u''
+
+
+class _ChangeNode(object):
+
+ def __init__(self, delimiters):
+ self.delimiters = delimiters
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ return u''
+
+
+class _EscapeNode(object):
+
+ def __init__(self, key):
+ self.key = key
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ s = engine.fetch_string(context, self.key)
+ return engine.escape(s)
+
+
+class _LiteralNode(object):
+
+ def __init__(self, key):
+ self.key = key
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ s = engine.fetch_string(context, self.key)
+ return engine.literal(s)
+
+
+class _PartialNode(object):
+
+ def __init__(self, key, indent):
+ self.key = key
+ self.indent = indent
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ template = engine.resolve_partial(self.key)
+ # Indent before rendering.
+ template = re.sub(NON_BLANK_RE, self.indent + ur'\1', template)
+
+ return engine.render(template, context)
+
+
+class _InvertedNode(object):
+
+ def __init__(self, key, parsed_section):
+ self.key = key
+ self.parsed_section = parsed_section
+
+ def __repr__(self):
+ return _format(self)
+
+ def render(self, engine, context):
+ # TODO: is there a bug because we are not using the same
+ # logic as in fetch_string()?
+ data = engine.resolve_context(context, self.key)
+ # Note that lambdas are considered truthy for inverted sections
+ # per the spec.
+ if data:
+ return u''
+ return self.parsed_section.render(engine, context)
+
+
+class _SectionNode(object):
+
+ # TODO: the template_ and parsed_template_ arguments don't both seem
+ # to be necessary. Can we remove one of them? For example, if
+ # callable(data) is True, then the initial parsed_template isn't used.
+ def __init__(self, key, parsed, delimiters, template, index_begin, index_end):
+ self.delimiters = delimiters
+ self.key = key
+ self.parsed = parsed
+ self.template = template
+ self.index_begin = index_begin
+ self.index_end = index_end
+
+ def __repr__(self):
+ return _format(self, exclude=['delimiters', 'template'])
+
+ def render(self, engine, context):
+ data = engine.fetch_section_data(context, self.key)
+
+ parts = []
+ for val in data:
+ if callable(val):
+ # Lambdas special case section rendering and bypass pushing
+ # the data value onto the context stack. From the spec--
+ #
+ # 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.
+ #
+ # Also see--
+ #
+ # https://github.com/defunkt/pystache/issues/113
+ #
+ # TODO: should we check the arity?
+ val = val(self.template[self.index_begin:self.index_end])
+ val = engine._render_value(val, context, delimiters=self.delimiters)
+ parts.append(val)
+ continue
+
+ context.push(val)
+ parts.append(self.parsed.render(engine, context))
+ context.pop()
+
+ return unicode(''.join(parts))
+
+
+class _Parser(object):
+
+ _delimiters = None
+ _template_re = None
+
+ def __init__(self, delimiters=None):
if delimiters is None:
- delimiters = DEFAULT_DELIMITERS
+ delimiters = DELIMITERS
self._delimiters = delimiters
- self.engine = engine
- def compile_template_re(self):
+ def _compile_delimiters(self):
self._template_re = _compile_template_re(self._delimiters)
def _change_delimiters(self, delimiters):
self._delimiters = delimiters
- self.compile_template_re()
+ self._compile_delimiters()
- def parse(self, template, start_index=0, section_key=None):
+ def parse(self, template):
"""
Parse a template string starting at some index.
@@ -98,11 +256,16 @@ class Parser(object):
a ParsedTemplate instance.
"""
- parse_tree = []
- index = start_index
+ self._compile_delimiters()
+
+ start_index = 0
+ content_end_index, parsed_section, section_key = None, None, None
+ parsed_template = ParsedTemplate()
+
+ states = []
while True:
- match = self._template_re.search(template, index)
+ match = self._template_re.search(template, start_index)
if match is None:
break
@@ -110,10 +273,6 @@ class Parser(object):
match_index = match.start()
end_index = match.end()
- before_tag = template[index : match_index]
-
- parse_tree.append(before_tag)
-
matches = match.groupdict()
# Normalize the matches dictionary.
@@ -138,100 +297,82 @@ class Parser(object):
if end_index < len(template):
end_index += template[end_index] == '\n' and 1 or 0
elif leading_whitespace:
- parse_tree.append(leading_whitespace)
match_index += len(leading_whitespace)
leading_whitespace = ''
- if tag_type == '/':
- if tag_key != section_key:
- raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != match_index:
+ parsed_template.add(template[start_index:match_index])
- return ParsedTemplate(parse_tree), match_index, end_index
+ start_index = end_index
- index = self._handle_tag_type(template, parse_tree, tag_type, tag_key, leading_whitespace, end_index)
+ if tag_type in ('#', '^'):
+ # Cache current state.
+ state = (tag_type, end_index, section_key, parsed_template)
+ states.append(state)
- # Save the rest of the template.
- parse_tree.append(template[index:])
+ # Initialize new state
+ section_key, parsed_template = tag_key, ParsedTemplate()
+ continue
- return ParsedTemplate(parse_tree)
-
- def _parse_section(self, template, start_index, section_key):
- """
- Parse the contents of a template section.
-
- Arguments:
-
- template: a unicode template string.
+ if tag_type == '/':
+ if tag_key != section_key:
+ raise ParsingError("Section end tag mismatch: %s != %s" % (tag_key, section_key))
- start_index: the string index at which the section contents begin.
+ # Restore previous state with newly found section data.
+ parsed_section = parsed_template
- section_key: the tag key of the section.
+ (tag_type, section_start_index, section_key, parsed_template) = states.pop()
+ node = self._make_section_node(template, tag_type, tag_key, parsed_section,
+ section_start_index, match_index)
- Returns: a 3-tuple:
+ else:
+ node = self._make_interpolation_node(tag_type, tag_key, leading_whitespace)
- parsed_section: the section contents parsed as a ParsedTemplate
- instance.
+ parsed_template.add(node)
- content_end_index: the string index after the section contents.
+ # Avoid adding spurious empty strings to the parse tree.
+ if start_index != len(template):
+ parsed_template.add(template[start_index:])
- end_index: the string index after the closing section tag (and
- including any trailing newlines).
+ return parsed_template
+ def _make_interpolation_node(self, tag_type, tag_key, leading_whitespace):
"""
- parsed_section, content_end_index, end_index = \
- self.parse(template=template, start_index=start_index, section_key=section_key)
-
- 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):
+ Create and return a non-section node for the parse tree.
+ """
# TODO: switch to using a dictionary instead of a bunch of ifs and elifs.
if tag_type == '!':
- return end_index
+ return _CommentNode()
if tag_type == '=':
delimiters = tag_key.split()
self._change_delimiters(delimiters)
- return end_index
-
- engine = self.engine
+ return _ChangeNode(delimiters)
if tag_type == '':
+ return _EscapeNode(tag_key)
- func = engine._make_get_escaped(tag_key)
-
- elif tag_type == '&':
+ if tag_type == '&':
+ return _LiteralNode(tag_key)
- func = engine._make_get_literal(tag_key)
+ if tag_type == '>':
+ return _PartialNode(tag_key, leading_whitespace)
- elif tag_type == '#':
+ raise Exception("Invalid symbol for interpolation tag: %s" % repr(tag_type))
- 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, section_contents, end_index = self._parse_section(template, end_index, tag_key)
- func = engine._make_get_inverse(tag_key, parsed_section)
-
- elif tag_type == '>':
-
- try:
- # TODO: make engine.load() and test it separately.
- template = engine.load_partial(tag_key)
- except TemplateNotFoundError:
- template = u''
-
- # Indent before rendering.
- template = re.sub(NON_BLANK_RE, leading_whitespace + ur'\1', template)
-
- func = engine._make_get_partial(template)
-
- else:
-
- raise Exception("Unrecognized tag type: %s" % repr(tag_type))
+ def _make_section_node(self, template, tag_type, tag_key, parsed_section,
+ section_start_index, section_end_index):
+ """
+ Create and return a section node for the parse tree.
- parse_tree.append(func)
+ """
+ if tag_type == '#':
+ return _SectionNode(tag_key, parsed_section, self._delimiters,
+ template, section_start_index, section_end_index)
- return end_index
+ if tag_type == '^':
+ return _InvertedNode(tag_key, parsed_section)
+ raise Exception("Invalid symbol for section tag: %s" % repr(tag_type))
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index bdbb30a..ef2c145 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -7,7 +7,16 @@ Defines a class responsible for rendering logic.
import re
-from pystache.parser import Parser
+from pystache.common import is_string
+from pystache.parser import parse
+
+
+def context_get(stack, name):
+ """
+ Find and return a name from a ContextStack instance.
+
+ """
+ return stack.get(name)
class RenderEngine(object):
@@ -29,15 +38,15 @@ class RenderEngine(object):
"""
- def __init__(self, load_partial=None, literal=None, escape=None):
+ # TODO: it would probably be better for the constructor to accept
+ # and set as an attribute a single RenderResolver instance
+ # that encapsulates the customizable aspects of converting
+ # strings and resolving partials and names from context.
+ def __init__(self, literal=None, escape=None, resolve_context=None,
+ resolve_partial=None):
"""
Arguments:
- load_partial: the function to call when loading a partial. The
- function should accept a string template name and return a
- template string of type unicode (not a subclass). If the
- template is not found, it should raise a TemplateNotFoundError.
-
literal: the function used to convert unescaped variable tag
values to unicode, e.g. the value corresponding to a tag
"{{{name}}}". The function should accept a string of type
@@ -59,217 +68,104 @@ class RenderEngine(object):
incoming strings of type markupsafe.Markup differently
from plain unicode strings.
+ resolve_context: the function to call to resolve a name against
+ a context stack. The function should accept two positional
+ arguments: a ContextStack instance and a name to resolve.
+
+ resolve_partial: the function to call when loading a partial.
+ The function should accept a template name string and return a
+ template string of type unicode (not a subclass).
+
"""
self.escape = escape
self.literal = literal
- self.load_partial = load_partial
-
- # TODO: rename context to stack throughout this module.
- def _get_string_value(self, context, tag_name):
+ self.resolve_context = resolve_context
+ self.resolve_partial = resolve_partial
+
+ # TODO: Rename context to stack throughout this module.
+
+ # From the spec:
+ #
+ # When used as the data value for an Interpolation tag, the lambda
+ # MUST be treatable as an arity 0 function, and invoked as such.
+ # The returned value MUST be rendered against the default delimiters,
+ # then interpolated in place of the lambda.
+ #
+ def fetch_string(self, context, name):
"""
Get a value from the given context as a basestring instance.
"""
- val = context.get(tag_name)
+ val = self.resolve_context(context, name)
if callable(val):
- # According to the spec:
- #
- # When used as the data value for an Interpolation tag,
- # the lambda MUST be treatable as an arity 0 function,
- # and invoked as such. The returned value MUST be
- # rendered against the default delimiters, then
- # interpolated in place of the lambda.
- template = val()
- if not isinstance(template, basestring):
- # In case the template is an integer, for example.
- template = str(template)
- if type(template) is not unicode:
- template = self.literal(template)
- val = self._render(template, context)
-
- if not isinstance(val, basestring):
- val = str(val)
+ # Return because _render_value() is already a string.
+ return self._render_value(val(), context)
+
+ if not is_string(val):
+ return str(val)
return val
- def _make_get_literal(self, name):
- def get_literal(context):
- """
- Returns: a string of type unicode.
-
- """
- s = self._get_string_value(context, name)
- s = self.literal(s)
- return s
-
- return get_literal
-
- def _make_get_escaped(self, name):
- get_literal = self._make_get_literal(name)
-
- def get_escaped(context):
- """
- Returns: a string of type unicode.
-
- """
- s = self._get_string_value(context, name)
- s = self.escape(s)
- return s
-
- return get_escaped
-
- def _make_get_partial(self, template):
- def get_partial(context):
- """
- Returns: a string of type unicode.
-
- """
- # TODO: the parsing should be done before calling this function.
- return self._render(template, context)
-
- return get_partial
-
- def _make_get_inverse(self, name, parsed_template):
- def get_inverse(context):
- """
- 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)
- # Per the spec, lambdas in inverted sections are considered truthy.
- if data:
- return u''
- return parsed_template.render(context)
-
- return get_inverse
-
- # TODO: the template_ and parsed_template_ arguments don't both seem
- # to be necessary. Can we remove one of them? For example, if
- # callable(data) is True, then the initial parsed_template isn't used.
- def _make_get_section(self, name, parsed_template_, template_, delims):
- def get_section(context):
- """
- Returns: a string of type unicode.
-
- """
- template = template_
- parsed_template = parsed_template_
- data = context.get(name)
-
- # From the spec:
+ def fetch_section_data(self, context, name):
+ data = self.resolve_context(context, name)
+
+ # From the spec:
+ #
+ # If the data is not of a list type, it is coerced into a list
+ # as follows: if the data is truthy (e.g. `!!data == true`),
+ # use a single-element list containing the data, otherwise use
+ # an empty list.
+ #
+ if not data:
+ data = []
+ else:
+ # The least brittle way to determine whether something
+ # supports iteration is by trying to call iter() on it:
#
- # If the data is not of a list type, it is coerced into a list
- # as follows: if the data is truthy (e.g. `!!data == true`),
- # use a single-element list containing the data, otherwise use
- # an empty list.
+ # http://docs.python.org/library/functions.html#iter
#
- if not data:
- data = []
+ # 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:
- # The least brittle way to determine 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.
+ if is_string(data) or isinstance(data, dict):
+ # Do not treat strings and dicts (which are iterable) as lists.
data = [data]
- else:
- if isinstance(data, (basestring, dict)):
- # Do not treat strings and dicts (which are iterable) as lists.
- data = [data]
- # Otherwise, treat the value as a list.
-
- parts = []
- for element in data:
- if callable(element):
- # Lambdas special case section rendering and bypass pushing
- # the data value onto the context stack. From the spec--
- #
- # 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.
- #
- # Also see--
- #
- # https://github.com/defunkt/pystache/issues/113
- #
- # TODO: should we check the arity?
- new_template = element(template)
- new_parsed_template = self._parse(new_template, delimiters=delims)
- parts.append(new_parsed_template.render(context))
- continue
-
- context.push(element)
- parts.append(parsed_template.render(context))
- context.pop()
-
- return unicode(''.join(parts))
-
- return get_section
-
- def _parse(self, template, delimiters=None):
- """
- Parse the given template, and return a ParsedTemplate instance.
-
- Arguments:
-
- template: a template string of type unicode.
-
- """
- parser = Parser(self, delimiters=delimiters)
- parser.compile_template_re()
+ # Otherwise, treat the value as a list.
- return parser.parse(template=template)
+ return data
- def _render(self, template, context):
+ def _render_value(self, val, context, delimiters=None):
"""
- Returns: a string of type unicode.
-
- Arguments:
-
- template: a template string of type unicode.
- context: a ContextStack instance.
+ Render an arbitrary value.
"""
- # We keep this type-check as an added check because this method is
- # called with template strings coming from potentially externally-
- # supplied functions like self.literal, self.load_partial, etc.
- # Beyond this point, we have much better control over the type.
- if type(template) is not unicode:
- raise Exception("Argument 'template' not unicode: %s: %s" % (type(template), repr(template)))
-
- parsed_template = self._parse(template)
-
- return parsed_template.render(context)
+ if not is_string(val):
+ # In case the template is an integer, for example.
+ val = str(val)
+ if type(val) is not unicode:
+ val = self.literal(val)
+ return self.render(val, context, delimiters)
- def render(self, template, context):
+ def render(self, template, context_stack, delimiters=None):
"""
- Return a template rendered as a string with type unicode.
+ Render a unicode template string, and return as unicode.
Arguments:
template: a template string of type unicode (but not a proper
subclass of unicode).
- context: a ContextStack instance.
+ context_stack: a ContextStack instance.
"""
- # Be strict but not too strict. In other words, accept str instead
- # of unicode, but don't assume anything about the encoding (e.g.
- # don't use self.literal).
- template = unicode(template)
+ parsed_template = parse(template, delimiters)
- return self._render(template, context)
+ return parsed_template.render(self, context_stack)
diff --git a/pystache/renderer.py b/pystache/renderer.py
index a3d4c57..20e4d48 100644
--- a/pystache/renderer.py
+++ b/pystache/renderer.py
@@ -8,25 +8,15 @@ This module provides a Renderer class to render templates.
import sys
from pystache import defaults
-from pystache.common import TemplateNotFoundError
-from pystache.context import ContextStack
+from pystache.common import TemplateNotFoundError, MissingTags, is_string
+from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader
-from pystache.renderengine import RenderEngine
+from pystache.parsed import ParsedTemplate
+from pystache.renderengine import context_get, RenderEngine
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):
"""
@@ -49,12 +39,35 @@ class Renderer(object):
def __init__(self, file_encoding=None, string_encoding=None,
decode_errors=None, search_dirs=None, file_extension=None,
- escape=None, partials=None):
+ escape=None, partials=None, missing_tags=None):
"""
Construct an instance.
Arguments:
+ file_encoding: the name of the encoding to use by default when
+ reading template files. All templates are converted to unicode
+ prior to parsing. Defaults to the package default.
+
+ string_encoding: the name of the encoding to use when converting
+ to unicode any byte strings (type str in Python 2) encountered
+ during the rendering process. This name will be passed as the
+ encoding argument to the built-in function unicode().
+ Defaults to the package default.
+
+ decode_errors: the string to pass as the errors argument to the
+ built-in function unicode() when converting byte strings to
+ unicode. Defaults to the package default.
+
+ search_dirs: the list of directories in which to search when
+ loading a template by name or file name. If given a string,
+ the method interprets the string as a single directory.
+ Defaults to the package default.
+
+ file_extension: the template file extension. Pass False for no
+ extension (i.e. to use extensionless template files).
+ Defaults to the package default.
+
partials: an object (e.g. a dictionary) for custom partial loading
during the rendering process.
The object should have a get() method that accepts a string
@@ -67,10 +80,6 @@ class Renderer(object):
the file system -- using relevant instance attributes like
search_dirs, file_encoding, etc.
- decode_errors: the string to pass as the errors argument to the
- built-in function unicode() when converting str strings to
- unicode. Defaults to the package default.
-
escape: the function used to escape variable tag values when
rendering a template. The function should accept a unicode
string (or subclass of unicode) and return an escaped string
@@ -84,24 +93,9 @@ class Renderer(object):
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
- to parsing. This encoding is used when reading template files
- and converting them to unicode. Defaults to the package default.
-
- file_extension: the template file extension. Pass False for no
- extension (i.e. to use extensionless template files).
- Defaults to the package default.
-
- search_dirs: the list of directories in which to search when
- loading a template by name or file name. If given a string,
- the method interprets the string as a single directory.
- Defaults to the package default.
-
- string_encoding: the name of the encoding to use when converting
- to unicode any strings of type str encountered during the
- rendering process. The name will be passed as the encoding
- argument to the built-in function unicode(). Defaults to the
+ missing_tags: a string specifying how to handle missing tags.
+ If 'strict', an error is raised on a missing tag. If 'ignore',
+ the value of the tag is the empty string. Defaults to the
package default.
"""
@@ -117,6 +111,9 @@ class Renderer(object):
if file_extension is None:
file_extension = defaults.TEMPLATE_EXTENSION
+ if missing_tags is None:
+ missing_tags = defaults.MISSING_TAGS
+
if search_dirs is None:
search_dirs = defaults.SEARCH_DIRS
@@ -131,6 +128,7 @@ class Renderer(object):
self.escape = escape
self.file_encoding = file_encoding
self.file_extension = file_extension
+ self.missing_tags = missing_tags
self.partials = partials
self.search_dirs = search_dirs
self.string_encoding = string_encoding
@@ -224,21 +222,21 @@ class Renderer(object):
def _make_load_partial(self):
"""
- Return the load_partial function to pass to RenderEngine.__init__().
+ Return a function that loads a partial by name.
"""
if self.partials is None:
- load_template = self._make_load_template()
- return load_template
+ return self._make_load_template()
- # Otherwise, create a load_partial function from the custom partial
- # loader that satisfies RenderEngine requirements (and that provides
- # a nicer exception, etc).
+ # Otherwise, create a function from the custom partial loader.
partials = self.partials
def load_partial(name):
+ # TODO: consider using EAFP here instead.
+ # http://docs.python.org/glossary.html#term-eafp
+ # This would mean requiring that the custom partial loader
+ # raise a KeyError on name not found.
template = partials.get(name)
-
if template is None:
raise TemplateNotFoundError("Name %s not found in partials: %s" %
(repr(name), type(partials)))
@@ -248,42 +246,78 @@ class Renderer(object):
return load_partial
- def _make_render_engine(self):
+ def _is_missing_tags_strict(self):
"""
- Return a RenderEngine instance for rendering.
+ Return whether missing_tags is set to strict.
+
+ """
+ val = self.missing_tags
+
+ if val == MissingTags.strict:
+ return True
+ elif val == MissingTags.ignore:
+ return False
+
+ raise Exception("Unsupported 'missing_tags' value: %s" % repr(val))
+
+ def _make_resolve_partial(self):
+ """
+ Return the resolve_partial function to pass to RenderEngine.__init__().
"""
load_partial = self._make_load_partial()
- engine = RenderEngine(load_partial=load_partial,
- literal=self._to_unicode_hard,
- escape=self._escape_to_unicode)
- return engine
+ if self._is_missing_tags_strict():
+ return load_partial
+ # Otherwise, ignore missing tags.
- # TODO: add unit tests for this method.
- def load_template(self, template_name):
+ def resolve_partial(name):
+ try:
+ return load_partial(name)
+ except TemplateNotFoundError:
+ return u''
+
+ return resolve_partial
+
+ def _make_resolve_context(self):
"""
- Load a template by name from the file system.
+ Return the resolve_context function to pass to RenderEngine.__init__().
"""
- load_template = self._make_load_template()
- return load_template(template_name)
+ if self._is_missing_tags_strict():
+ return context_get
+ # Otherwise, ignore missing tags.
- def _render_string(self, template, *context, **kwargs):
+ def resolve_context(stack, name):
+ try:
+ return context_get(stack, name)
+ except KeyNotFoundError:
+ return u''
+
+ return resolve_context
+
+ def _make_render_engine(self):
"""
- Render the given template string using the given context.
+ Return a RenderEngine instance for rendering.
"""
- # RenderEngine.render() requires that the template string be unicode.
- template = self._to_unicode_hard(template)
+ resolve_context = self._make_resolve_context()
+ resolve_partial = self._make_resolve_partial()
- context = ContextStack.create(*context, **kwargs)
- self._context = context
+ engine = RenderEngine(literal=self._to_unicode_hard,
+ escape=self._escape_to_unicode,
+ resolve_context=resolve_context,
+ resolve_partial=resolve_partial)
+ return engine
- engine = self._make_render_engine()
- rendered = engine.render(template, context)
+ # TODO: add unit tests for this method.
+ def load_template(self, template_name):
+ """
+ Load a template by name from the file system.
- return unicode(rendered)
+ """
+ load_template = self._make_load_template()
+ return load_template(template_name)
def _render_object(self, obj, *context, **kwargs):
"""
@@ -319,24 +353,54 @@ class Renderer(object):
return self._render_string(template, *context, **kwargs)
+ def _render_string(self, template, *context, **kwargs):
+ """
+ Render the given template string using the given context.
+
+ """
+ # RenderEngine.render() requires that the template string be unicode.
+ template = self._to_unicode_hard(template)
+
+ render_func = lambda engine, stack: engine.render(template, stack)
+
+ return self._render_final(render_func, *context, **kwargs)
+
+ # All calls to render() should end here because it prepares the
+ # context stack correctly.
+ def _render_final(self, render_func, *context, **kwargs):
+ """
+ Arguments:
+
+ render_func: a function that accepts a RenderEngine and ContextStack
+ instance and returns a template rendering as a unicode string.
+
+ """
+ stack = ContextStack.create(*context, **kwargs)
+ self._context = stack
+
+ engine = self._make_render_engine()
+
+ return render_func(engine, stack)
+
def render(self, template, *context, **kwargs):
"""
- Render the given template (or template object) using the given context.
+ Render the given template string, view template, or parsed template.
- Returns the rendering as a unicode string.
+ Returns a unicode string.
- Prior to rendering, templates of type str are converted to unicode
- using the string_encoding and decode_errors attributes. See the
- constructor docstring for more information.
+ Prior to rendering, this method will convert a template that is a
+ byte string (type str in Python 2) to unicode using the string_encoding
+ and decode_errors attributes. See the constructor docstring for
+ more information.
Arguments:
- template: a template string of type unicode or str, or an object
- instance. If the argument is an object, the function first looks
- for the template associated to the object by calling this class's
- get_associated_template() method. The rendering process also
- uses the passed object as the first element of the context stack
- when rendering.
+ template: a template string that is unicode or a byte string,
+ a ParsedTemplate instance, or another object instance. In the
+ final case, the function first looks for the template associated
+ to the object by calling this class's get_associated_template()
+ method. The rendering process also uses the passed object as
+ the first element of the context stack when rendering.
*context: zero or more dictionaries, ContextStack instances, or objects
with which to populate the initial context stack. None
@@ -350,8 +414,11 @@ class Renderer(object):
all items in the *context list.
"""
- if isinstance(template, _STRING_TYPES):
+ if is_string(template):
return self._render_string(template, *context, **kwargs)
+ if isinstance(template, ParsedTemplate):
+ render_func = lambda engine, stack: template.render(engine, stack)
+ return self._render_final(render_func, *context, **kwargs)
# Otherwise, we assume the template is an object.
return self._render_object(template, *context, **kwargs)
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
index 24b24dc..307a2be 100644
--- a/pystache/tests/common.py
+++ b/pystache/tests/common.py
@@ -22,7 +22,7 @@ PROJECT_DIR = os.path.join(PACKAGE_DIR, '..')
SPEC_TEST_DIR = os.path.join(PROJECT_DIR, 'ext', 'spec', 'specs')
# TEXT_DOCTEST_PATHS: the paths to text files (i.e. non-module files)
# containing doctests. The paths should be relative to the project directory.
-TEXT_DOCTEST_PATHS = ['README.rst']
+TEXT_DOCTEST_PATHS = ['README.md']
UNITTEST_FILE_PREFIX = "test_"
diff --git a/pystache/tests/doctesting.py b/pystache/tests/doctesting.py
index 469c81e..1102b78 100644
--- a/pystache/tests/doctesting.py
+++ b/pystache/tests/doctesting.py
@@ -44,7 +44,11 @@ def get_doctests(text_file_dir):
paths = [os.path.normpath(os.path.join(text_file_dir, path)) for path in TEXT_DOCTEST_PATHS]
if sys.version_info >= (3,):
- paths = _convert_paths(paths)
+ # Skip the README doctests in Python 3 for now because examples
+ # rendering to unicode do not give consistent results
+ # (e.g. 'foo' vs u'foo').
+ # paths = _convert_paths(paths)
+ paths = []
suites = []
diff --git a/pystache/tests/main.py b/pystache/tests/main.py
index de56c44..6b463af 100644
--- a/pystache/tests/main.py
+++ b/pystache/tests/main.py
@@ -10,7 +10,7 @@ This module is for our test console script.
import os
import sys
import unittest
-from unittest import TestProgram
+from unittest import TestCase, TestProgram
import pystache
from pystache.tests.common import PACKAGE_DIR, PROJECT_DIR, SPEC_TEST_DIR, UNITTEST_FILE_PREFIX
@@ -24,6 +24,58 @@ from pystache.tests.spectesting import get_spec_tests
FROM_SOURCE_OPTION = "--from-source"
+def make_extra_tests(text_doctest_dir, spec_test_dir):
+ tests = []
+
+ if text_doctest_dir is not None:
+ doctest_suites = get_doctests(text_doctest_dir)
+ tests.extend(doctest_suites)
+
+ if spec_test_dir is not None:
+ spec_testcases = get_spec_tests(spec_test_dir)
+ tests.extend(spec_testcases)
+
+ return unittest.TestSuite(tests)
+
+
+def make_test_program_class(extra_tests):
+ """
+ Return a subclass of unittest.TestProgram.
+
+ """
+ # The function unittest.main() is an alias for unittest.TestProgram's
+ # constructor. TestProgram's constructor does the following:
+ #
+ # 1. calls self.parseArgs(argv),
+ # 2. which in turn calls self.createTests().
+ # 3. then the constructor calls self.runTests().
+ #
+ # The createTests() method sets the self.test attribute by calling one
+ # of self.testLoader's "loadTests" methods. Each loadTest method returns
+ # a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
+ # instance prior to calling runTests().
+ class PystacheTestProgram(TestProgram):
+
+ """
+ Instantiating an instance of this class runs all tests.
+
+ """
+
+ def createTests(self):
+ """
+ Load tests and set self.test to a unittest.TestSuite instance
+
+ Compare--
+
+ http://docs.python.org/library/unittest.html#unittest.TestSuite
+
+ """
+ super(PystacheTestProgram, self).createTests()
+ self.test.addTests(extra_tests)
+
+ return PystacheTestProgram
+
+
# Do not include "test" in this function's name to avoid it getting
# picked up by nosetests.
def main(sys_argv):
@@ -71,16 +123,17 @@ def main(sys_argv):
# Add the current module for unit tests contained here.
sys_argv.append(__name__)
- _PystacheTestProgram._text_doctest_dir = project_dir
- _PystacheTestProgram._spec_test_dir = spec_test_dir
SetupTests.project_dir = project_dir
+ extra_tests = make_extra_tests(project_dir, spec_test_dir)
+ test_program_class = make_test_program_class(extra_tests)
+
# We pass None for the module because we do not want the unittest
# module to resolve module names relative to a given module.
# (This would require importing all of the unittest modules from
# this module.) See the loadTestsFromName() method of the
# unittest.TestLoader class for more details on this parameter.
- _PystacheTestProgram(argv=sys_argv, module=None)
+ test_program_class(argv=sys_argv, module=None)
# No need to return since unitttest.main() exits.
@@ -103,7 +156,7 @@ def _discover_test_modules(package_dir):
return names
-class SetupTests(unittest.TestCase):
+class SetupTests(TestCase):
"""Tests about setup.py."""
@@ -123,33 +176,3 @@ class SetupTests(unittest.TestCase):
self.assertEqual(VERSION, pystache.__version__)
finally:
sys.path = original_path
-
-
-# The function unittest.main() is an alias for unittest.TestProgram's
-# constructor. TestProgram's constructor calls self.runTests() as its
-# final step, which expects self.test to be set. The constructor sets
-# the self.test attribute by calling one of self.testLoader's "loadTests"
-# methods prior to callint self.runTests(). Each loadTest method returns
-# a unittest.TestSuite instance. Thus, self.test is set to a TestSuite
-# instance prior to calling runTests().
-class _PystacheTestProgram(TestProgram):
-
- """
- Instantiating an instance of this class runs all tests.
-
- """
-
- def runTests(self):
- # self.test is a unittest.TestSuite instance:
- # http://docs.python.org/library/unittest.html#unittest.TestSuite
- tests = self.test
-
- if self._text_doctest_dir is not None:
- doctest_suites = get_doctests(self._text_doctest_dir)
- tests.addTests(doctest_suites)
-
- if self._spec_test_dir is not None:
- spec_testcases = get_spec_tests(self._spec_test_dir)
- tests.addTests(spec_testcases)
-
- TestProgram.runTests(self)
diff --git a/pystache/tests/test___init__.py b/pystache/tests/test___init__.py
index d4f3526..eae42c1 100644
--- a/pystache/tests/test___init__.py
+++ b/pystache/tests/test___init__.py
@@ -23,7 +23,7 @@ class InitTests(unittest.TestCase):
"""
actual = set(GLOBALS_PYSTACHE_IMPORTED) - set(GLOBALS_INITIAL)
- expected = set(['render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
+ expected = set(['parse', 'render', 'Renderer', 'TemplateSpec', 'GLOBALS_INITIAL'])
self.assertEqual(actual, expected)
diff --git a/pystache/tests/test_context.py b/pystache/tests/test_context.py
index d432428..238e4b0 100644
--- a/pystache/tests/test_context.py
+++ b/pystache/tests/test_context.py
@@ -8,10 +8,8 @@ Unit tests of context.py.
from datetime import datetime
import unittest
-from pystache.context import _NOT_FOUND
-from pystache.context import _get_value
-from pystache.context import ContextStack
-from pystache.tests.common import AssertIsMixin, AssertStringMixin, Attachable
+from pystache.context import _NOT_FOUND, _get_value, KeyNotFoundError, ContextStack
+from pystache.tests.common import AssertIsMixin, AssertStringMixin, AssertExceptionMixin, Attachable
class SimpleObject(object):
@@ -39,7 +37,7 @@ class DictLike(object):
return self._dict[key]
-class GetValueTests(unittest.TestCase, AssertIsMixin):
+class GetValueTestCase(unittest.TestCase, AssertIsMixin):
"""Test context._get_value()."""
@@ -147,6 +145,26 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertEqual(item["foo"], "bar")
self.assertNotFound(item, "foo")
+ def test_object__property__raising_exception(self):
+ """
+ Test getting a property that raises an exception.
+
+ """
+ class Foo(object):
+
+ @property
+ def bar(self):
+ return 1
+
+ @property
+ def baz(self):
+ raise ValueError("test")
+
+ foo = Foo()
+ self.assertEqual(_get_value(foo, 'bar'), 1)
+ self.assertNotFound(foo, 'missing')
+ self.assertRaises(ValueError, _get_value, foo, 'baz')
+
### Case: the item is an instance of a built-in type.
def test_built_in_type__integer(self):
@@ -204,7 +222,8 @@ class GetValueTests(unittest.TestCase, AssertIsMixin):
self.assertNotFound(item2, 'pop')
-class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class ContextStackTestCase(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertExceptionMixin):
"""
Test the ContextStack class.
@@ -306,6 +325,24 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
context = ContextStack.create({'foo': 'bar'}, foo='buzz')
self.assertEqual(context.get('foo'), 'buzz')
+ ## Test the get() method.
+
+ def test_get__single_dot(self):
+ """
+ Test getting a single dot (".").
+
+ """
+ context = ContextStack("a", "b")
+ self.assertEqual(context.get("."), "b")
+
+ def test_get__single_dot__missing(self):
+ """
+ Test getting a single dot (".") with an empty context stack.
+
+ """
+ context = ContextStack()
+ self.assertException(KeyNotFoundError, "Key '.' not found: empty context stack", context.get, ".")
+
def test_get__key_present(self):
"""
Test getting a key.
@@ -320,15 +357,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
context = ContextStack()
- self.assertString(context.get("foo"), u'')
-
- def test_get__default(self):
- """
- Test that get() respects the default value.
-
- """
- context = ContextStack()
- self.assertEqual(context.get("foo", "bar"), "bar")
+ self.assertException(KeyNotFoundError, "Key 'foo' not found: first part", context.get, "foo")
def test_get__precedence(self):
"""
@@ -424,10 +453,10 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
def test_dot_notation__missing_attr_or_key(self):
name = "foo.bar.baz.bak"
stack = ContextStack({"foo": {"bar": {}}})
- self.assertString(stack.get(name), u'')
+ self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
stack = ContextStack({"foo": Attachable(bar=Attachable())})
- self.assertString(stack.get(name), u'')
+ self.assertException(KeyNotFoundError, "Key 'foo.bar.baz.bak' not found: missing 'baz'", stack.get, name)
def test_dot_notation__missing_part_terminates_search(self):
"""
@@ -451,7 +480,7 @@ class ContextStackTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
"""
stack = ContextStack({'a': {'b': 'A.B'}}, {'a': 'A'})
self.assertEqual(stack.get('a'), 'A')
- self.assertString(stack.get('a.b'), u'')
+ self.assertException(KeyNotFoundError, "Key 'a.b' not found: missing 'b'", stack.get, "a.b")
stack.pop()
self.assertEqual(stack.get('a.b'), 'A.B')
diff --git a/pystache/tests/test_locator.py b/pystache/tests/test_locator.py
index f17a289..8d4ec9b 100644
--- a/pystache/tests/test_locator.py
+++ b/pystache/tests/test_locator.py
@@ -53,7 +53,17 @@ class LocatorTests(unittest.TestCase, AssertExceptionMixin):
def test_get_object_directory__not_hasattr_module(self):
locator = Locator()
- obj = datetime(2000, 1, 1)
+ # Previously, we used a genuine object -- a datetime instance --
+ # because datetime instances did not have the __module__ attribute
+ # in CPython. See, for example--
+ #
+ # http://bugs.python.org/issue15223
+ #
+ # However, since datetime instances do have the __module__ attribute
+ # in PyPy, we needed to switch to something else once we added
+ # support for PyPi. This was so that our test runs would pass
+ # in all systems.
+ obj = "abc"
self.assertFalse(hasattr(obj, '__module__'))
self.assertEqual(locator.get_object_directory(obj), None)
diff --git a/pystache/tests/test_parser.py b/pystache/tests/test_parser.py
index 4aa0959..92248ea 100644
--- a/pystache/tests/test_parser.py
+++ b/pystache/tests/test_parser.py
@@ -7,6 +7,7 @@ Unit tests of parser.py.
import unittest
+from pystache.defaults import DELIMITERS
from pystache.parser import _compile_template_re as make_re
@@ -19,7 +20,7 @@ class RegularExpressionTestCase(unittest.TestCase):
Test getting a key from a dictionary.
"""
- re = make_re()
+ re = make_re(DELIMITERS)
match = re.search("b {{test}}")
self.assertEqual(match.start(), 1)
diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
index b13e246..4c40c47 100644
--- a/pystache/tests/test_renderengine.py
+++ b/pystache/tests/test_renderengine.py
@@ -5,13 +5,23 @@ Unit tests of renderengine.py.
"""
+import sys
import unittest
-from pystache.context import ContextStack
+from pystache.context import ContextStack, KeyNotFoundError
from pystache import defaults
from pystache.parser import ParsingError
-from pystache.renderengine import RenderEngine
-from pystache.tests.common import AssertStringMixin, Attachable
+from pystache.renderer import Renderer
+from pystache.renderengine import context_get, RenderEngine
+from pystache.tests.common import AssertStringMixin, AssertExceptionMixin, Attachable
+
+
+def _get_unicode_char():
+ if sys.version_info < (3, ):
+ return 'u'
+ return ''
+
+_UNICODE_CHAR = _get_unicode_char()
def mock_literal(s):
@@ -45,14 +55,14 @@ class RenderEngineTestCase(unittest.TestCase):
"""
# In real-life, these arguments would be functions
- engine = RenderEngine(load_partial="foo", literal="literal", escape="escape")
+ engine = RenderEngine(resolve_partial="foo", literal="literal", escape="escape")
self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal")
- self.assertEqual(engine.load_partial, "foo")
+ self.assertEqual(engine.resolve_partial, "foo")
-class RenderTests(unittest.TestCase, AssertStringMixin):
+class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
"""
Tests RenderEngine.render().
@@ -68,8 +78,9 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
Create and return a default RenderEngine for testing.
"""
- escape = defaults.TAG_ESCAPE
- engine = RenderEngine(literal=unicode, escape=escape, load_partial=None)
+ renderer = Renderer(string_encoding='utf-8', missing_tags='strict')
+ engine = renderer._make_render_engine()
+
return engine
def _assert_render(self, expected, template, *context, **kwargs):
@@ -81,25 +92,26 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
engine = kwargs.get('engine', self._engine())
if partials is not None:
- engine.load_partial = lambda key: unicode(partials[key])
+ engine.resolve_partial = lambda key: unicode(partials[key])
context = ContextStack(*context)
- actual = engine.render(template, context)
+ # RenderEngine.render() only accepts unicode template strings.
+ actual = engine.render(unicode(template), context)
self.assertString(actual=actual, expected=expected)
def test_render(self):
self._assert_render(u'Hi Mom', 'Hi {{person}}', {'person': 'Mom'})
- def test__load_partial(self):
+ def test__resolve_partial(self):
"""
Test that render() uses the load_template attribute.
"""
engine = self._engine()
partials = {'partial': u"{{person}}"}
- engine.load_partial = lambda key: partials[key]
+ engine.resolve_partial = lambda key: partials[key]
self._assert_render(u'Hi Mom', 'Hi {{>partial}}', {'person': 'Mom'}, engine=engine)
@@ -285,6 +297,16 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'section': item, attr_name: 7}
self._assert_render(u'7', template, context)
+ # This test is also important for testing 2to3.
+ def test_interpolation__nonascii_nonunicode(self):
+ """
+ Test a tag whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
def test_implicit_iterator__literal(self):
"""
Test an implicit iterator in a literal tag.
@@ -343,6 +365,28 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
self._assert_render(u'unescaped: < escaped: &lt;', template, context, engine=engine, partials=partials)
+ ## Test cases related specifically to lambdas.
+
+ # This test is also important for testing 2to3.
+ def test_section__nonascii_nonunicode(self):
+ """
+ Test a section whose value is a non-ascii, non-unicode string.
+
+ """
+ template = '{{#nonascii}}{{.}}{{/nonascii}}'
+ context = {'nonascii': u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ # This test is also important for testing 2to3.
+ def test_lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda tag value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{lambda}}'
+ context = {'lambda': lambda: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
## Test cases related specifically to sections.
def test_section__end_tag_with_no_start_tag(self):
@@ -461,6 +505,25 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'test': (lambda text: 'Hi %s' % text)}
self._assert_render(u'Hi Mom', template, context)
+ # This test is also important for testing 2to3.
+ def test_section__lambda__returning_nonascii_nonunicode(self):
+ """
+ Test a lambda section value returning a non-ascii, non-unicode string.
+
+ """
+ template = '{{#lambda}}{{/lambda}}'
+ context = {'lambda': lambda text: u'abcdé'.encode('utf-8')}
+ self._assert_render(u'abcdé', template, context)
+
+ def test_section__lambda__returning_nonstring(self):
+ """
+ Test a lambda section value returning a non-string.
+
+ """
+ template = '{{#lambda}}foo{{/lambda}}'
+ context = {'lambda': lambda text: len(text)}
+ self._assert_render(u'3', template, context)
+
def test_section__iterable(self):
"""
Check that objects supporting iteration (aside from dicts) behave like lists.
@@ -609,33 +672,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
context = {'person': person}
self._assert_render(u'Hello, Biggles. I see you are 42.', template, context)
- def test_dot_notation__missing_attributes_or_keys(self):
- """
- Test dot notation with missing keys or attributes.
-
- Check that if a key or attribute in a dotted name does not exist, then
- the tag renders as the empty string.
-
- """
- template = """I cannot see {{person.name}}'s age: {{person.age}}.
- Nor {{other_person.name}}'s: ."""
- expected = u"""I cannot see Biggles's age: .
- Nor Mr. Bradshaw's: ."""
- context = {'person': {'name': 'Biggles'},
- 'other_person': Attachable(name='Mr. Bradshaw')}
- self._assert_render(expected, template, context)
-
def test_dot_notation__multiple_levels(self):
"""
Test dot notation with multiple levels.
"""
template = """Hello, Mr. {{person.name.lastname}}.
- I see you're back from {{person.travels.last.country.city}}.
- I'm missing some of your details: {{person.details.private.editor}}."""
+ I see you're back from {{person.travels.last.country.city}}."""
expected = u"""Hello, Mr. Pither.
- I see you're back from Cornwall.
- I'm missing some of your details: ."""
+ I see you're back from Cornwall."""
context = {'person': {'name': {'firstname': 'unknown', 'lastname': 'Pither'},
'travels': {'last': {'country': {'city': 'Cornwall'}}},
'details': {'public': 'likes cycling'}}}
@@ -667,6 +712,15 @@ class RenderTests(unittest.TestCase, AssertStringMixin):
https://github.com/mustache/spec/pull/48
"""
- template = '{{a.b}} :: ({{#c}}{{a}} :: {{a.b}}{{/c}})'
context = {'a': {'b': 'A.B'}, 'c': {'a': 'A'} }
- self._assert_render(u'A.B :: (A :: )', template, context)
+
+ template = '{{a.b}}'
+ self._assert_render(u'A.B', template, context)
+
+ template = '{{#c}}{{a}}{{/c}}'
+ self._assert_render(u'A', template, context)
+
+ template = '{{#c}}{{a.b}}{{/c}}'
+ self.assertException(KeyNotFoundError, "Key %(unicode)s'a.b' not found: missing %(unicode)s'b'" %
+ {'unicode': _UNICODE_CHAR},
+ self._assert_render, 'A.B :: (A :: )', template, context)
diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py
index f04c799..69cc64d 100644
--- a/pystache/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -14,6 +14,7 @@ from examples.simple import Simple
from pystache import Renderer
from pystache import TemplateSpec
from pystache.common import TemplateNotFoundError
+from pystache.context import ContextStack, KeyNotFoundError
from pystache.loader import Loader
from pystache.tests.common import get_data_path, AssertStringMixin, AssertExceptionMixin
@@ -124,6 +125,22 @@ class RendererInitTestCase(unittest.TestCase):
renderer = Renderer(file_extension='foo')
self.assertEqual(renderer.file_extension, 'foo')
+ def test_missing_tags(self):
+ """
+ Check that the missing_tags attribute is set correctly.
+
+ """
+ renderer = Renderer(missing_tags='foo')
+ self.assertEqual(renderer.missing_tags, 'foo')
+
+ def test_missing_tags__default(self):
+ """
+ Check the missing_tags default.
+
+ """
+ renderer = Renderer()
+ self.assertEqual(renderer.missing_tags, 'ignore')
+
def test_search_dirs__default(self):
"""
Check the search_dirs default.
@@ -319,37 +336,37 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
renderer.string_encoding = 'utf_8'
self.assertEqual(renderer.render(template), u"déf")
- def test_make_load_partial(self):
+ def test_make_resolve_partial(self):
"""
- Test the _make_load_partial() method.
+ Test the _make_resolve_partial() method.
"""
renderer = Renderer()
renderer.partials = {'foo': 'bar'}
- load_partial = renderer._make_load_partial()
+ resolve_partial = renderer._make_resolve_partial()
- actual = load_partial('foo')
+ actual = resolve_partial('foo')
self.assertEqual(actual, 'bar')
self.assertEqual(type(actual), unicode, "RenderEngine requires that "
- "load_partial return unicode strings.")
+ "resolve_partial return unicode strings.")
- def test_make_load_partial__unicode(self):
+ def test_make_resolve_partial__unicode(self):
"""
- Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
+ Test _make_resolve_partial(): that resolve_partial doesn't "double-decode" Unicode.
"""
renderer = Renderer()
renderer.partials = {'partial': 'foo'}
- load_partial = renderer._make_load_partial()
- self.assertEqual(load_partial("partial"), "foo")
+ resolve_partial = renderer._make_resolve_partial()
+ self.assertEqual(resolve_partial("partial"), "foo")
# Now with a value that is already unicode.
renderer.partials = {'partial': u'foo'}
- load_partial = renderer._make_load_partial()
+ resolve_partial = renderer._make_resolve_partial()
# If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported
- self.assertEqual(load_partial("partial"), "foo")
+ self.assertEqual(resolve_partial("partial"), "foo")
def test_render_path(self):
"""
@@ -406,7 +423,7 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
# we no longer need to exercise all rendering code paths through
# the Renderer. It suffices to test rendering paths through the
# RenderEngine for the same amount of code coverage.
-class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
+class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
"""
Check the RenderEngine returned by Renderer._make_render_engine().
@@ -420,11 +437,11 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
"""
return _make_renderer()
- ## Test the engine's load_partial attribute.
+ ## Test the engine's resolve_partial attribute.
- def test__load_partial__returns_unicode(self):
+ def test__resolve_partial__returns_unicode(self):
"""
- Check that load_partial returns unicode (and not a subclass).
+ Check that resolve_partial returns unicode (and not a subclass).
"""
class MyUnicode(unicode):
@@ -436,43 +453,70 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
engine = renderer._make_render_engine()
- actual = engine.load_partial('str')
+ actual = engine.resolve_partial('str')
self.assertEqual(actual, "foo")
self.assertEqual(type(actual), unicode)
# Check that unicode subclasses are not preserved.
- actual = engine.load_partial('subclass')
+ actual = engine.resolve_partial('subclass')
self.assertEqual(actual, "abc")
self.assertEqual(type(actual), unicode)
- def test__load_partial__not_found__default(self):
+ def test__resolve_partial__not_found(self):
+ """
+ Check that resolve_partial returns the empty string when a template is not found.
+
+ """
+ renderer = Renderer()
+
+ engine = renderer._make_render_engine()
+ resolve_partial = engine.resolve_partial
+
+ self.assertString(resolve_partial('foo'), u'')
+
+ def test__resolve_partial__not_found__missing_tags_strict(self):
"""
- Check that load_partial provides a nice message when a template is not found.
+ Check that resolve_partial provides a nice message when a template is not found.
"""
renderer = Renderer()
+ renderer.missing_tags = 'strict'
engine = renderer._make_render_engine()
- load_partial = engine.load_partial
+ resolve_partial = engine.resolve_partial
self.assertException(TemplateNotFoundError, "File 'foo.mustache' not found in dirs: ['.']",
- load_partial, "foo")
+ resolve_partial, "foo")
+
+ def test__resolve_partial__not_found__partials_dict(self):
+ """
+ Check that resolve_partial returns the empty string when a template is not found.
+
+ """
+ renderer = Renderer()
+ renderer.partials = {}
+
+ engine = renderer._make_render_engine()
+ resolve_partial = engine.resolve_partial
+
+ self.assertString(resolve_partial('foo'), u'')
- def test__load_partial__not_found__dict(self):
+ def test__resolve_partial__not_found__partials_dict__missing_tags_strict(self):
"""
- Check that load_partial provides a nice message when a template is not found.
+ Check that resolve_partial provides a nice message when a template is not found.
"""
renderer = Renderer()
+ renderer.missing_tags = 'strict'
renderer.partials = {}
engine = renderer._make_render_engine()
- load_partial = engine.load_partial
+ resolve_partial = engine.resolve_partial
- # Include dict directly since str(dict) is different in Python 2 and 3:
- # <type 'dict'> versus <class 'dict'>, respectively.
+ # Include dict directly since str(dict) is different in Python 2 and 3:
+ # <type 'dict'> versus <class 'dict'>, respectively.
self.assertException(TemplateNotFoundError, "Name 'foo' not found in partials: %s" % dict,
- load_partial, "foo")
+ resolve_partial, "foo")
## Test the engine's literal attribute.
@@ -595,3 +639,47 @@ class Renderer_MakeRenderEngineTests(unittest.TestCase, AssertExceptionMixin):
self.assertTrue(isinstance(s, unicode))
self.assertEqual(type(escape(s)), unicode)
+ ## Test the missing_tags attribute.
+
+ def test__missing_tags__unknown_value(self):
+ """
+ Check missing_tags attribute: setting an unknown value.
+
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'foo'
+
+ self.assertException(Exception, "Unsupported 'missing_tags' value: 'foo'",
+ renderer._make_render_engine)
+
+ ## Test the engine's resolve_context attribute.
+
+ def test__resolve_context(self):
+ """
+ Check resolve_context(): default arguments.
+
+ """
+ renderer = Renderer()
+
+ engine = renderer._make_render_engine()
+
+ stack = ContextStack({'foo': 'bar'})
+
+ self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
+ self.assertString(u'', engine.resolve_context(stack, 'missing'))
+
+ def test__resolve_context__missing_tags_strict(self):
+ """
+ Check resolve_context(): missing_tags 'strict'.
+
+ """
+ renderer = Renderer()
+ renderer.missing_tags = 'strict'
+
+ engine = renderer._make_render_engine()
+
+ stack = ContextStack({'foo': 'bar'})
+
+ self.assertEqual('bar', engine.resolve_context(stack, 'foo'))
+ self.assertException(KeyNotFoundError, "Key 'missing' not found: first part",
+ engine.resolve_context, stack, 'missing')