From f285f4aaae2a54cb35157eec7058b7941fcf3802 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 20 Oct 2012 11:16:00 -0700 Subject: Address issue #130: allow string coercion to be customized. --- HISTORY.md | 2 ++ pystache/renderengine.py | 12 +++++++--- pystache/renderer.py | 35 ++++++++++++++++++++++++----- pystache/tests/test_renderengine.py | 45 ++++++++++++++++++++++++++++++++++++- pystache/tests/test_renderer.py | 33 +++++++++++++++++++++++++++ 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 -- cgit v1.2.1