diff options
Diffstat (limited to 'pystache')
-rw-r--r-- | pystache/loader.py | 2 | ||||
-rw-r--r-- | pystache/parsed.py | 24 | ||||
-rw-r--r-- | pystache/parser.py | 4 | ||||
-rw-r--r-- | pystache/renderengine.py | 16 | ||||
-rw-r--r-- | pystache/renderer.py | 46 | ||||
-rw-r--r-- | pystache/specloader.py | 3 | ||||
-rw-r--r-- | pystache/template_spec.py | 30 | ||||
-rw-r--r-- | pystache/tests/common.py | 8 | ||||
-rw-r--r-- | pystache/tests/test_renderengine.py | 45 | ||||
-rw-r--r-- | pystache/tests/test_renderer.py | 40 | ||||
-rw-r--r-- | pystache/tests/test_specloader.py | 32 |
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() |