summaryrefslogtreecommitdiff
path: root/pystache
diff options
context:
space:
mode:
Diffstat (limited to 'pystache')
-rw-r--r--pystache/loader.py2
-rw-r--r--pystache/parsed.py24
-rw-r--r--pystache/parser.py4
-rw-r--r--pystache/renderengine.py16
-rw-r--r--pystache/renderer.py46
-rw-r--r--pystache/specloader.py3
-rw-r--r--pystache/template_spec.py30
-rw-r--r--pystache/tests/common.py8
-rw-r--r--pystache/tests/test_renderengine.py45
-rw-r--r--pystache/tests/test_renderer.py40
-rw-r--r--pystache/tests/test_specloader.py32
11 files changed, 205 insertions, 45 deletions
diff --git a/pystache/loader.py b/pystache/loader.py
index 5855392..d4a7e53 100644
--- a/pystache/loader.py
+++ b/pystache/loader.py
@@ -33,6 +33,8 @@ class Loader(object):
"""
Loads the template associated to a name or user-defined object.
+ All load_*() methods return the template as a unicode string.
+
"""
def __init__(self, file_encoding=None, extension=None, to_unicode=None,
diff --git a/pystache/parsed.py b/pystache/parsed.py
index e94c644..372d96c 100644
--- a/pystache/parsed.py
+++ b/pystache/parsed.py
@@ -8,6 +8,16 @@ Exposes a class that represents a parsed (or compiled) template.
class ParsedTemplate(object):
+ """
+ Represents a parsed or compiled template.
+
+ An instance wraps a list of unicode strings and node objects. A node
+ object must have a `render(engine, stack)` method that accepts a
+ RenderEngine instance and a ContextStack instance and returns a unicode
+ string.
+
+ """
+
def __init__(self):
self._parse_tree = []
@@ -18,10 +28,8 @@ class ParsedTemplate(object):
"""
Arguments:
- 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.
+ node: a unicode string or node object instance. See the class
+ docstring for information.
"""
self._parse_tree.append(node)
@@ -32,10 +40,10 @@ class ParsedTemplate(object):
"""
# We avoid use of the ternary operator for Python 2.4 support.
- def get_unicode(val):
- if type(val) is unicode:
- return val
- return val.render(engine, context)
+ def get_unicode(node):
+ if type(node) is unicode:
+ return node
+ return node.render(engine, context)
parts = map(get_unicode, self._parse_tree)
s = ''.join(parts)
diff --git a/pystache/parser.py b/pystache/parser.py
index 8b82776..c6a171f 100644
--- a/pystache/parser.py
+++ b/pystache/parser.py
@@ -189,10 +189,10 @@ class _SectionNode(object):
return _format(self, exclude=['delimiters', 'template'])
def render(self, engine, context):
- data = engine.fetch_section_data(context, self.key)
+ values = engine.fetch_section_data(context, self.key)
parts = []
- for val in data:
+ for val in values:
if callable(val):
# Lambdas special case section rendering and bypass pushing
# the data value onto the context stack. From the spec--
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index ef2c145..c797b17 100644
--- a/pystache/renderengine.py
+++ b/pystache/renderengine.py
@@ -43,7 +43,7 @@ class RenderEngine(object):
# 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):
+ resolve_partial=None, to_str=None):
"""
Arguments:
@@ -76,11 +76,17 @@ class RenderEngine(object):
The function should accept a template name string and return a
template string of type unicode (not a subclass).
+ to_str: a function that accepts an object and returns a string (e.g.
+ the built-in function str). This function is used for string
+ coercion whenever a string is required (e.g. for converting None
+ or 0 to a string).
+
"""
self.escape = escape
self.literal = literal
self.resolve_context = resolve_context
self.resolve_partial = resolve_partial
+ self.to_str = to_str
# TODO: Rename context to stack throughout this module.
@@ -103,11 +109,15 @@ class RenderEngine(object):
return self._render_value(val(), context)
if not is_string(val):
- return str(val)
+ return self.to_str(val)
return val
def fetch_section_data(self, context, name):
+ """
+ Fetch the value of a section as a list.
+
+ """
data = self.resolve_context(context, name)
# From the spec:
@@ -149,7 +159,7 @@ class RenderEngine(object):
"""
if not is_string(val):
# In case the template is an integer, for example.
- val = str(val)
+ val = self.to_str(val)
if type(val) is not unicode:
val = self.literal(val)
return self.render(val, context, delimiters)
diff --git a/pystache/renderer.py b/pystache/renderer.py
index 20e4d48..ff6a90c 100644
--- a/pystache/renderer.py
+++ b/pystache/renderer.py
@@ -23,11 +23,11 @@ class Renderer(object):
A class for rendering mustache templates.
This class supports several rendering options which are described in
- the constructor's docstring. Among these, the constructor supports
- passing a custom partial loader.
+ the constructor's docstring. Other behavior can be customized by
+ subclassing this class.
- Here is an example of rendering a template using a custom partial loader
- that loads partials from a string-string dictionary.
+ For example, one can pass a string-string dictionary to the constructor
+ to bypass loading partials from the file system:
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(partials=partials)
@@ -35,6 +35,16 @@ class Renderer(object):
>>> print renderer.render('{{>partial}}', {'thing': 'world'})
Hello, world!
+ To customize string coercion (e.g. to render False values as ''), one can
+ subclass this class. For example:
+
+ class MyRenderer(Renderer):
+ def str_coerce(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
"""
def __init__(self, file_encoding=None, string_encoding=None,
@@ -146,6 +156,20 @@ class Renderer(object):
"""
return self._context
+ # We could not choose str() as the name because 2to3 renames the unicode()
+ # method of this class to str().
+ def str_coerce(self, val):
+ """
+ Coerce a non-string value to a string.
+
+ This method is called whenever a non-string is encountered during the
+ rendering process when a string is needed (e.g. if a context value
+ for string interpolation is not a string). To customize string
+ coercion, you can override this method.
+
+ """
+ return str(val)
+
def _to_unicode_soft(self, s):
"""
Convert a basestring to unicode, preserving any unicode subclass.
@@ -307,7 +331,8 @@ class Renderer(object):
engine = RenderEngine(literal=self._to_unicode_hard,
escape=self._escape_to_unicode,
resolve_context=resolve_context,
- resolve_partial=resolve_partial)
+ resolve_partial=resolve_partial,
+ to_str=self.str_coerce)
return engine
# TODO: add unit tests for this method.
@@ -341,6 +366,17 @@ class Renderer(object):
return self._render_string(template, *context, **kwargs)
+ def render_name(self, template_name, *context, **kwargs):
+ """
+ Render the template with the given name using the given context.
+
+ See the render() docstring for more information.
+
+ """
+ loader = self._make_loader()
+ template = loader.load_name(template_name)
+ return self._render_string(template, *context, **kwargs)
+
def render_path(self, template_path, *context, **kwargs):
"""
Render the template at the given path using the given context.
diff --git a/pystache/specloader.py b/pystache/specloader.py
index 3cb0f1a..3a77d4c 100644
--- a/pystache/specloader.py
+++ b/pystache/specloader.py
@@ -55,6 +55,9 @@ class SpecLoader(object):
Find and return the path to the template associated to the instance.
"""
+ if spec.template_path is not None:
+ return spec.template_path
+
dir_path, file_name = self._find_relative(spec)
locator = self.loader._make_locator()
diff --git a/pystache/template_spec.py b/pystache/template_spec.py
index 76ce784..9e9f454 100644
--- a/pystache/template_spec.py
+++ b/pystache/template_spec.py
@@ -4,12 +4,11 @@
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".
+from a class that subclasses TemplateSpec. The "spec" in TemplateSpec
+stands for "special" or "specified" template information.
"""
-# TODO: finish the class docstring.
class TemplateSpec(object):
"""
@@ -28,20 +27,27 @@ class TemplateSpec(object):
template: the template as a string.
- template_rel_path: the path to the template file, relative to the
- directory containing the module defining the class.
-
- template_rel_directory: the directory containing the template file, relative
- to the directory containing the module defining the class.
+ template_encoding: the encoding used by the template.
template_extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files).
+ template_name: the name of the template.
+
+ template_path: absolute path to the template.
+
+ template_rel_directory: the directory containing the template file,
+ relative to the directory containing the module defining the class.
+
+ template_rel_path: the path to the template file, relative to the
+ directory containing the module defining the class.
+
"""
template = None
- template_rel_path = None
- template_rel_directory = None
- template_name = None
- template_extension = None
template_encoding = None
+ template_extension = None
+ template_name = None
+ template_path = None
+ template_rel_directory = None
+ template_rel_path = None
diff --git a/pystache/tests/common.py b/pystache/tests/common.py
index 307a2be..99be4c8 100644
--- a/pystache/tests/common.py
+++ b/pystache/tests/common.py
@@ -43,7 +43,10 @@ def html_escape(u):
return u.replace("'", ''')
-def get_data_path(file_name):
+def get_data_path(file_name=None):
+ """Return the path to a file in the test data directory."""
+ if file_name is None:
+ file_name = ""
return os.path.join(DATA_DIR, file_name)
@@ -139,8 +142,7 @@ class AssertStringMixin:
format = "%s"
# Show both friendly and literal versions.
- details = """String mismatch: %%s\
-
+ details = """String mismatch: %%s
Expected: \"""%s\"""
Actual: \"""%s\"""
diff --git a/pystache/tests/test_renderengine.py b/pystache/tests/test_renderengine.py
index 4c40c47..db916f7 100644
--- a/pystache/tests/test_renderengine.py
+++ b/pystache/tests/test_renderengine.py
@@ -55,11 +55,13 @@ class RenderEngineTestCase(unittest.TestCase):
"""
# In real-life, these arguments would be functions
- engine = RenderEngine(resolve_partial="foo", literal="literal", escape="escape")
+ engine = RenderEngine(resolve_partial="foo", literal="literal",
+ escape="escape", to_str="str")
self.assertEqual(engine.escape, "escape")
self.assertEqual(engine.literal, "literal")
self.assertEqual(engine.resolve_partial, "foo")
+ self.assertEqual(engine.to_str, "str")
class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
@@ -182,6 +184,47 @@ class RenderTests(unittest.TestCase, AssertStringMixin, AssertExceptionMixin):
self._assert_render(u'**bar bar**', template, context, engine=engine)
+ # Custom to_str for testing purposes.
+ def _to_str(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ def test_to_str(self):
+ """Test the to_str attribute."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__lambda(self):
+ """Test the to_str attribute for a lambda."""
+ engine = self._engine()
+ template = '{{value}}'
+ context = {'value': lambda: None}
+
+ self._assert_render(u'None', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_list(self):
+ """Test the to_str attribute for a section list."""
+ engine = self._engine()
+ template = '{{#list}}{{.}}{{/list}}'
+ context = {'list': [None, None]}
+
+ self._assert_render(u'NoneNone', template, context, engine=engine)
+ engine.to_str = self._to_str
+ self._assert_render(u'', template, context, engine=engine)
+
+ def test_to_str__section_lambda(self):
+ # TODO: add a test for a "method with an arity of 1".
+ pass
+
def test__non_basestring__literal_and_escaped(self):
"""
Test a context value that is not a basestring instance.
diff --git a/pystache/tests/test_renderer.py b/pystache/tests/test_renderer.py
index 69cc64d..0dbe0d9 100644
--- a/pystache/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -368,6 +368,13 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
# TypeError: decoding Unicode is not supported
self.assertEqual(resolve_partial("partial"), "foo")
+ def test_render_name(self):
+ """Test the render_name() method."""
+ data_dir = get_data_path()
+ renderer = Renderer(search_dirs=data_dir)
+ actual = renderer.render_name("say_hello", to='foo')
+ self.assertString(actual, u"Hello, foo")
+
def test_render_path(self):
"""
Test the render_path() method.
@@ -418,6 +425,39 @@ class RendererTests(unittest.TestCase, AssertStringMixin):
actual = renderer.render(view)
self.assertEqual('Hi pizza!', actual)
+ def test_custom_string_coercion_via_assignment(self):
+ """
+ Test that string coercion can be customized via attribute assignment.
+
+ """
+ renderer = self._renderer()
+ def to_str(val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+
+ self.assertEqual(renderer.render('{{value}}', value=None), 'None')
+ renderer.str_coerce = to_str
+ self.assertEqual(renderer.render('{{value}}', value=None), '')
+
+ def test_custom_string_coercion_via_subclassing(self):
+ """
+ Test that string coercion can be customized via subclassing.
+
+ """
+ class MyRenderer(Renderer):
+ def str_coerce(self, val):
+ if not val:
+ return ''
+ else:
+ return str(val)
+ renderer1 = Renderer()
+ renderer2 = MyRenderer()
+
+ self.assertEqual(renderer1.render('{{value}}', value=None), 'None')
+ self.assertEqual(renderer2.render('{{value}}', value=None), '')
+
# By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through
diff --git a/pystache/tests/test_specloader.py b/pystache/tests/test_specloader.py
index 24fb34d..d934987 100644
--- a/pystache/tests/test_specloader.py
+++ b/pystache/tests/test_specloader.py
@@ -30,6 +30,14 @@ class Thing(object):
pass
+class AssertPathsMixin:
+
+ """A unittest.TestCase mixin to check path equality."""
+
+ def assertPaths(self, actual, expected):
+ self.assertEqual(actual, expected)
+
+
class ViewTestCase(unittest.TestCase, AssertStringMixin):
def test_template_rel_directory(self):
@@ -174,7 +182,8 @@ def _make_specloader():
return SpecLoader(loader=loader)
-class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
+class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin,
+ AssertPathsMixin):
"""
Tests template_spec.SpecLoader.
@@ -288,13 +297,21 @@ class SpecLoaderTests(unittest.TestCase, AssertIsMixin, AssertStringMixin):
self.assertEqual(loader.s, "template-foo")
self.assertEqual(loader.encoding, "encoding-foo")
+ def test_find__template_path(self):
+ """Test _find() with TemplateSpec.template_path."""
+ loader = self._make_specloader()
+ custom = TemplateSpec()
+ custom.template_path = "path/foo"
+ actual = loader._find(custom)
+ self.assertPaths(actual, "path/foo")
+
# TODO: migrate these tests into the SpecLoaderTests class.
# TODO: rename the get_template() tests to test load().
# TODO: condense, reorganize, and rename the tests so that it is
# clear whether we have full test coverage (e.g. organized by
# TemplateSpec attributes or something).
-class TemplateSpecTests(unittest.TestCase):
+class TemplateSpecTests(unittest.TestCase, AssertPathsMixin):
def _make_loader(self):
return _make_specloader()
@@ -358,13 +375,6 @@ class TemplateSpecTests(unittest.TestCase):
view.template_extension = 'txt'
self._assert_template_location(view, (None, 'sample_view.txt'))
- def _assert_paths(self, actual, expected):
- """
- Assert that two paths are the same.
-
- """
- self.assertEqual(actual, expected)
-
def test_find__with_directory(self):
"""
Test _find() with a view that has a directory specified.
@@ -379,7 +389,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'foo/bar.txt')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def test_find__without_directory(self):
"""
@@ -394,7 +404,7 @@ class TemplateSpecTests(unittest.TestCase):
actual = loader._find(view)
expected = os.path.join(DATA_DIR, 'sample_view.mustache')
- self._assert_paths(actual, expected)
+ self.assertPaths(actual, expected)
def _assert_get_template(self, custom, expected):
loader = self._make_loader()