Commit 8fb3cda2 by Chris Jerdonek

Merge 'issue_64' into development: closing issue #64 (Loader.get())

The Renderer class now accepts a loader implementing get()
(e.g. a dictionary or Loader instance) rather than a load_template
function.
parents 62d1264b f8f8cc93
...@@ -18,6 +18,7 @@ API changes: ...@@ -18,6 +18,7 @@ API changes:
* ``Template.render()`` now accepts the context to render instead of * ``Template.render()`` now accepts the context to render instead of
``Template()``. [cjerdonek] ``Template()``. [cjerdonek]
* ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek]
Bug fixes: Bug fixes:
......
...@@ -55,7 +55,7 @@ def main(sys_argv): ...@@ -55,7 +55,7 @@ def main(sys_argv):
template = template[:-9] template = template[:-9]
try: try:
template = Loader().load_template(template) template = Loader().get(template)
except IOError: except IOError:
pass pass
......
...@@ -19,22 +19,27 @@ class Loader(object): ...@@ -19,22 +19,27 @@ class Loader(object):
Arguments: Arguments:
search_dirs: the list of directories in which to search for templates,
for example when looking for partials. Defaults to the current
working directory. If given a string, the string is interpreted
as a single directory.
extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files).
encoding: the name of the encoding to use when converting file encoding: the name of the encoding to use when converting file
contents to unicode. This name will be passed as the encoding contents to unicode. This name will be passed as the encoding
argument to the built-in function unicode(). Defaults to the argument to the built-in function unicode(). Defaults to the
encoding name returned by sys.getdefaultencoding(). encoding name returned by sys.getdefaultencoding().
search_dirs: the directories in which to search for templates.
Defaults to the current working directory.
extension: the template file extension. Defaults to "mustache".
Pass False for no extension.
""" """
if encoding is None: if encoding is None:
encoding = sys.getdefaultencoding() encoding = sys.getdefaultencoding()
if extension is None: if extension is None:
extension = DEFAULT_EXTENSION extension = DEFAULT_EXTENSION
if search_dirs is None: if search_dirs is None:
search_dirs = os.curdir # i.e. "." search_dirs = os.curdir # i.e. "."
...@@ -52,7 +57,7 @@ class Loader(object): ...@@ -52,7 +57,7 @@ class Loader(object):
return file_name return file_name
def load_template(self, template_name): def get(self, template_name):
""" """
Find and load the given template, and return it as a string. Find and load the given template, and return it as a string.
......
...@@ -22,34 +22,57 @@ except ImportError: ...@@ -22,34 +22,57 @@ except ImportError:
class Renderer(object): class Renderer(object):
# TODO: change load_template to load_partial. """
def __init__(self, load_template=None, output_encoding=None, escape=None, A class for rendering mustache templates.
default_encoding=None, decode_errors='strict'):
This class supports several rendering options which are described in
the constructor's docstring. Among these, the constructor supports
passing a custom template loader.
Here is an example of passing a custom template loader to render a
template using partials loaded from a string-string dictionary.
>>> partials = {'partial': 'Hello, {{thing}}!'}
>>> renderer = Renderer(loader=partials)
>>> renderer.render('{{>partial}}', {'thing': 'world'})
u'Hello, world!'
"""
def __init__(self, loader=None, default_encoding=None, decode_errors='strict',
output_encoding=None, escape=None):
""" """
Construct an instance. Construct an instance.
Arguments: Arguments:
load_template: a function for loading templates by name, for loader: the object (e.g. pystache.Loader or dictionary) that will
example when loading partials. The function should accept a load templates during the rendering process, for example when
single template_name parameter and return a template as a string. loading a partial.
Defaults to the default Loader's load_template() method. The loader should have a get() method that accepts a string
and returns the corresponding template as a string, preferably
as a unicode string. If there is no template with that name,
the method should either return None (as dict.get() does) or
raise an exception.
Defaults to constructing a Loader instance with
default_encoding passed as the encoding argument.
output_encoding: the encoding to use when rendering to a string. output_encoding: the encoding to use when rendering to a string.
The argument should be the name of an encoding as a string, for The argument should be the name of an encoding as a string, for
example "utf-8". See the render() method's documentation for more example "utf-8". See the render() method's documentation for
information. more information.
escape: the function used to escape mustache variable values escape: the function used to escape mustache variable values
when rendering a template. The function should accept a unicode when rendering a template. The function should accept a
string and return an escaped string of the same type. It need unicode string and return an escaped string of the same type.
not handle strings of type `str` because this class will only This function need not handle strings of type `str` because
pass it unicode strings. The constructor assigns this escape this class will only pass it unicode strings. The constructor
function to the constructed instance's Template.escape() method. assigns this function to the constructed instance's escape()
method.
The argument defaults to markupsafe.escape when markupsafe is The argument defaults to markupsafe.escape when markupsafe
importable and cgi.escape otherwise. To disable escaping entirely, is importable and cgi.escape otherwise. To disable escaping
one can pass `lambda s: s` as the escape function, for example. entirely, one can pass `lambda u: u` as the escape function,
for example.
default_encoding: the name of the encoding to use when converting default_encoding: the name of the encoding to use when converting
to unicode any strings of type `str` encountered during the to unicode any strings of type `str` encountered during the
...@@ -63,16 +86,15 @@ class Renderer(object): ...@@ -63,16 +86,15 @@ class Renderer(object):
Defaults to "strict". Defaults to "strict".
""" """
if load_template is None:
loader = Loader()
load_template = loader.load_template
if default_encoding is None: if default_encoding is None:
default_encoding = sys.getdefaultencoding() default_encoding = sys.getdefaultencoding()
if escape is None: if escape is None:
escape = markupsafe.escape if markupsafe else cgi.escape escape = markupsafe.escape if markupsafe else cgi.escape
if loader is None:
loader = Loader(encoding=default_encoding)
literal = markupsafe.Markup if markupsafe else unicode literal = markupsafe.Markup if markupsafe else unicode
self._literal = literal self._literal = literal
...@@ -80,7 +102,7 @@ class Renderer(object): ...@@ -80,7 +102,7 @@ class Renderer(object):
self.decode_errors = decode_errors self.decode_errors = decode_errors
self.default_encoding = default_encoding self.default_encoding = default_encoding
self.escape = escape self.escape = escape
self.load_template = load_template self.loader = loader
self.output_encoding = output_encoding self.output_encoding = output_encoding
def _unicode_and_escape(self, s): def _unicode_and_escape(self, s):
...@@ -139,11 +161,11 @@ class Renderer(object): ...@@ -139,11 +161,11 @@ class Renderer(object):
""" """
def load_partial(name): def load_partial(name):
template = self.load_template(name) template = self.loader.get(name)
# Make sure the return value of load_template is unicode since # Make sure the return value is unicode since RenderEngine requires
# RenderEngine requires it. Also, check that the string is not # it. Also, check that the string is not already unicode to
# already unicode to avoid "double-decoding". Otherwise, we # avoid "double-decoding". Otherwise, we would get the following
# would get the following error: # error:
# TypeError: decoding Unicode is not supported # TypeError: decoding Unicode is not supported
if not isinstance(template, unicode): if not isinstance(template, unicode):
template = self.unicode(template) template = self.unicode(template)
...@@ -168,19 +190,19 @@ class Renderer(object): ...@@ -168,19 +190,19 @@ class Renderer(object):
""" """
Render the given template using the given context. Render the given template using the given context.
The return value is a unicode string, unless the output_encoding Returns:
attribute has been set to a non-None value, in which case the
return value has type str and is encoded using that encoding.
If the template string is not unicode, it is first converted to If the output_encoding attribute is None, the return value is
unicode using the default_encoding and decode_errors attributes. a unicode string. Otherwise, the return value is encoded to a
See the Template constructor's docstring for more information. string of type str using the output encoding named by the
output_encoding attribute.
Arguments: Arguments:
template: a template string that is either unicode, or of type template: a template string that is either unicode or of type str.
str and encoded using the encoding named by the default_encoding If the string has type str, it is first converted to unicode
keyword argument. using the default_encoding and decode_errors attributes of this
instance. See the constructor docstring for more information.
context: a dictionary, Context, or object (e.g. a View instance). context: a dictionary, Context, or object (e.g. a View instance).
......
...@@ -21,16 +21,24 @@ class View(object): ...@@ -21,16 +21,24 @@ class View(object):
template_encoding = None template_encoding = None
template_extension = None template_extension = None
# A function that accepts a single template_name parameter. _loader = None
_load_template = None
def __init__(self, template=None, context=None, load_template=None, **kwargs): def __init__(self, template=None, context=None, loader=None, **kwargs):
""" """
Construct a View instance. Construct a View instance.
Arguments:
loader: the object (e.g. pystache.Loader or dictionary) responsible
for loading templates during the rendering process, for example
when loading partials. The object should have a get() method
that accepts a string and returns the corresponding template
as a string, preferably as a unicode string. The method should
return None if there is no template with that name.
""" """
if load_template is not None: if loader is not None:
self._load_template = load_template self._loader = loader
if template is not None: if template is not None:
self.template = template self.template = template
...@@ -43,17 +51,21 @@ class View(object): ...@@ -43,17 +51,21 @@ class View(object):
self.context = _context self.context = _context
def load_template(self, template_name): def get_loader(self):
if self._load_template is None: if self._loader is None:
# We delay setting self._load_template until now (in the case # We delay setting self._loader until now (in the case that the
# that the user did not supply a load_template to the constructor) # user did not supply a load_template to the constructor)
# to let users set the template_extension attribute, etc. after # to let users set the template_extension attribute, etc. after
# View.__init__() has already been called. # View.__init__() has already been called.
loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding, loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding,
extension=self.template_extension) extension=self.template_extension)
self._load_template = loader.load_template self._loader = loader
return self._load_template(template_name) return self._loader
def load_template(self, template_name):
loader = self.get_loader()
return loader.get(template_name)
def get_template(self): def get_template(self):
""" """
...@@ -98,8 +110,9 @@ class View(object): ...@@ -98,8 +110,9 @@ class View(object):
Return the view rendered using the current context. Return the view rendered using the current context.
""" """
loader = self.get_loader()
template = self.get_template() template = self.get_template()
renderer = Renderer(self.load_template, output_encoding=encoding, escape=escape) renderer = Renderer(output_encoding=encoding, escape=escape, loader=loader)
return renderer.render(template, self.context) return renderer.render(template, self.context)
def get(self, key, default=None): def get(self, key, default=None):
......
...@@ -48,36 +48,36 @@ class LoaderTestCase(unittest.TestCase): ...@@ -48,36 +48,36 @@ class LoaderTestCase(unittest.TestCase):
loader.template_extension = '' loader.template_extension = ''
self.assertEquals(loader.make_file_name('foo'), 'foo.') self.assertEquals(loader.make_file_name('foo'), 'foo.')
def test_template_is_loaded(self): def test_get__template_is_loaded(self):
loader = Loader(search_dirs='examples') loader = Loader(search_dirs='examples')
template = loader.load_template('simple') template = loader.get('simple')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}') self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
def test_using_list_of_paths(self): def test_get__using_list_of_paths(self):
loader = Loader(search_dirs=['doesnt_exist', 'examples']) loader = Loader(search_dirs=['doesnt_exist', 'examples'])
template = loader.load_template('simple') template = loader.get('simple')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}') self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
def test_non_existent_template_fails(self): def test_get__non_existent_template_fails(self):
loader = Loader() loader = Loader()
self.assertRaises(IOError, loader.load_template, 'doesnt_exist') self.assertRaises(IOError, loader.get, 'doesnt_exist')
def test_load_template__extensionless_file(self): def test_get__extensionless_file(self):
loader = Loader(search_dirs=self.search_dirs) loader = Loader(search_dirs=self.search_dirs)
self.assertRaises(IOError, loader.load_template, 'extensionless') self.assertRaises(IOError, loader.get, 'extensionless')
loader.template_extension = False loader.template_extension = False
self.assertEquals(loader.load_template('extensionless'), "No file extension: {{foo}}") self.assertEquals(loader.get('extensionless'), "No file extension: {{foo}}")
def test_load_template__unicode_return_value(self): def test_get__load_template__unicode_return_value(self):
""" """
Check that load_template() returns unicode strings. Check that load_template() returns unicode strings.
""" """
loader = Loader(search_dirs=self.search_dirs) loader = Loader(search_dirs=self.search_dirs)
template = loader.load_template('simple') template = loader.get('simple')
self.assertEqual(type(template), unicode) self.assertEqual(type(template), unicode)
...@@ -11,6 +11,37 @@ import unittest ...@@ -11,6 +11,37 @@ import unittest
from pystache import renderer from pystache import renderer
from pystache.renderer import Renderer from pystache.renderer import Renderer
from pystache.loader import Loader
class RendererInitTestCase(unittest.TestCase):
"""A class to test the Renderer.__init__() method."""
def test_loader(self):
"""Test that the loader attribute is set correctly."""
loader = {'foo': 'bar'}
r = Renderer(loader=loader)
self.assertEquals(r.loader, {'foo': 'bar'})
def test_loader__default(self):
"""Test that the default loader is constructed correctly."""
r = Renderer()
actual = r.loader
expected = Loader()
self.assertEquals(type(actual), type(expected))
self.assertEquals(actual.__dict__, expected.__dict__)
def test_loader__default__default_encoding(self):
"""Test that the default loader inherits the default_encoding."""
r = Renderer(default_encoding='foo')
actual = r.loader
expected = Loader(encoding='foo')
self.assertEquals(actual.template_encoding, expected.template_encoding)
# Check all attributes for good measure.
self.assertEquals(actual.__dict__, expected.__dict__)
class RendererTestCase(unittest.TestCase): class RendererTestCase(unittest.TestCase):
...@@ -240,19 +271,37 @@ class RendererTestCase(unittest.TestCase): ...@@ -240,19 +271,37 @@ class RendererTestCase(unittest.TestCase):
renderer.default_encoding = 'utf_8' renderer.default_encoding = 'utf_8'
self.assertEquals(renderer.render(template), "déf") self.assertEquals(renderer.render(template), "déf")
def test_make_load_partial(self):
"""
Test the _make_load_partial() method.
"""
partials = {'foo': 'bar'}
renderer = Renderer(loader=partials)
load_partial = renderer._make_load_partial()
actual = load_partial('foo')
self.assertEquals(actual, 'bar')
self.assertEquals(type(actual), unicode, "RenderEngine requires that "
"load_partial return unicode strings.")
def test_make_load_partial__unicode(self): def test_make_load_partial__unicode(self):
""" """
Test that the generated load_partial does not "double-decode" Unicode. Test _make_load_partial(): that load_partial doesn't "double-decode" Unicode.
""" """
renderer = Renderer() renderer = Renderer()
# In real-life, the partial would be different with each name.
renderer.load_template = lambda name: u"partial"
renderer.loader = {'partial': 'foo'}
load_partial = renderer._make_load_partial() load_partial = renderer._make_load_partial()
self.assertEquals(load_partial("partial"), "foo")
# This would raise a TypeError exception if we tried to double-decode. # Now with a value that is already unicode.
self.assertEquals(load_partial("test"), "partial") renderer.loader = {'partial': u'foo'}
load_partial = renderer._make_load_partial()
# If the next line failed, we would get the following error:
# TypeError: decoding Unicode is not supported
self.assertEquals(load_partial("partial"), "foo")
# By testing that Renderer.render() constructs the RenderEngine instance # By testing that Renderer.render() constructs the RenderEngine instance
# correctly, we no longer need to test the rendering code paths through # correctly, we no longer need to test the rendering code paths through
...@@ -263,14 +312,13 @@ class RendererTestCase(unittest.TestCase): ...@@ -263,14 +312,13 @@ class RendererTestCase(unittest.TestCase):
Test that _make_render_engine() constructs and passes load_partial correctly. Test that _make_render_engine() constructs and passes load_partial correctly.
""" """
renderer = Renderer() partials = {'partial': 'foo'}
renderer = Renderer(loader=partials)
renderer.unicode = lambda s: s.upper() # a test version. renderer.unicode = lambda s: s.upper() # a test version.
# In real-life, the partial would be different with each name.
renderer.load_template = lambda name: "partial"
engine = renderer._make_render_engine() engine = renderer._make_render_engine()
# Make sure it calls unicode. # Make sure it calls unicode.
self.assertEquals(engine.load_partial('name'), "PARTIAL") self.assertEquals(engine.load_partial('partial'), "FOO")
def test_make_render_engine__literal(self): def test_make_render_engine__literal(self):
""" """
......
...@@ -60,15 +60,13 @@ class ViewTestCase(unittest.TestCase): ...@@ -60,15 +60,13 @@ class ViewTestCase(unittest.TestCase):
template = view.load_template('extensionless') template = view.load_template('extensionless')
self.assertEquals(template, "No file extension: {{foo}}") self.assertEquals(template, "No file extension: {{foo}}")
def test_custom_load_template(self): def test_load_template__custom_loader(self):
""" """
Test passing a custom load_template to View.__init__(). Test passing a custom loader to View.__init__().
""" """
partials_dict = {"partial": "Loaded from dictionary"} partials = {"partial": "Loaded from dictionary"}
load_template = lambda template_name: partials_dict[template_name] view = Simple(loader=partials)
view = Simple(load_template=load_template)
actual = view.load_template("partial") actual = view.load_template("partial")
self.assertEquals(actual, "Loaded from dictionary") self.assertEquals(actual, "Loaded from dictionary")
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment