summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2012-10-20 11:16:00 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2012-10-20 11:16:00 -0700
commitf285f4aaae2a54cb35157eec7058b7941fcf3802 (patch)
tree0d8b610635c7733ef4c1c94b1df66b6ab2287163
parent5b00b6cd392a1f5f317cc18893198bb5d62c565d (diff)
downloadpystache-f285f4aaae2a54cb35157eec7058b7941fcf3802.tar.gz
Address issue #130: allow string coercion to be customized.
-rw-r--r--HISTORY.md2
-rw-r--r--pystache/renderengine.py12
-rw-r--r--pystache/renderer.py35
-rw-r--r--pystache/tests/test_renderengine.py45
-rw-r--r--pystache/tests/test_renderer.py33
5 files changed, 118 insertions, 9 deletions
diff --git a/HISTORY.md b/HISTORY.md
index 477c8a6..662f117 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,6 +4,8 @@ History
0.5.3 (TBD)
-----------
+- Added ability to customize string coercion (e.g. to have None render as
+ `''`) (issue \#130).
- Added Renderer.render_name() to render a template by name (issue \#122).
- Added TemplateSpec.template_path to specify an absolute path to a
template (issue \#41).
diff --git a/pystache/renderengine.py b/pystache/renderengine.py
index 83cd24e..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,7 +109,7 @@ class RenderEngine(object):
return self._render_value(val(), context)
if not is_string(val):
- return str(val)
+ return self.to_str(val)
return val
@@ -153,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 49be4a0..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.
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 df518f9..0dbe0d9 100644
--- a/pystache/tests/test_renderer.py
+++ b/pystache/tests/test_renderer.py
@@ -425,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