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:
* ``Template.render()`` now accepts the context to render instead of
``Template()``. [cjerdonek]
* ``Loader.load_template()`` changed to ``Loader.get()``. [cjerdonek]
Bug fixes:
......
......@@ -55,7 +55,7 @@ def main(sys_argv):
template = template[:-9]
try:
template = Loader().load_template(template)
template = Loader().get(template)
except IOError:
pass
......
......@@ -19,22 +19,27 @@ class Loader(object):
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
contents to unicode. This name will be passed as the encoding
argument to the built-in function unicode(). Defaults to the
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:
encoding = sys.getdefaultencoding()
if extension is None:
extension = DEFAULT_EXTENSION
if search_dirs is None:
search_dirs = os.curdir # i.e. "."
......@@ -52,7 +57,7 @@ class Loader(object):
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.
......
......@@ -22,34 +22,57 @@ except ImportError:
class Renderer(object):
# TODO: change load_template to load_partial.
def __init__(self, load_template=None, output_encoding=None, escape=None,
default_encoding=None, decode_errors='strict'):
"""
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 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.
Arguments:
load_template: a function for loading templates by name, for
example when loading partials. The function should accept a
single template_name parameter and return a template as a string.
Defaults to the default Loader's load_template() method.
loader: the object (e.g. pystache.Loader or dictionary) that will
load templates during the rendering process, for example when
loading a partial.
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.
The argument should be the name of an encoding as a string, for
example "utf-8". See the render() method's documentation for more
information.
example "utf-8". See the render() method's documentation for
more information.
escape: the function used to escape mustache variable values
when rendering a template. The function should accept a unicode
string and return an escaped string of the same type. It need
not handle strings of type `str` because this class will only
pass it unicode strings. The constructor assigns this escape
function to the constructed instance's Template.escape() method.
The argument defaults to markupsafe.escape when markupsafe is
importable and cgi.escape otherwise. To disable escaping entirely,
one can pass `lambda s: s` as the escape function, for example.
when rendering a template. The function should accept a
unicode string and return an escaped string of the same type.
This function need not handle strings of type `str` because
this class will only pass it unicode strings. The constructor
assigns this function to the constructed instance's escape()
method.
The argument defaults to markupsafe.escape when markupsafe
is importable and cgi.escape otherwise. To disable escaping
entirely, one can pass `lambda u: u` as the escape function,
for example.
default_encoding: the name of the encoding to use when converting
to unicode any strings of type `str` encountered during the
......@@ -63,16 +86,15 @@ class Renderer(object):
Defaults to "strict".
"""
if load_template is None:
loader = Loader()
load_template = loader.load_template
if default_encoding is None:
default_encoding = sys.getdefaultencoding()
if escape is None:
escape = markupsafe.escape if markupsafe else cgi.escape
if loader is None:
loader = Loader(encoding=default_encoding)
literal = markupsafe.Markup if markupsafe else unicode
self._literal = literal
......@@ -80,7 +102,7 @@ class Renderer(object):
self.decode_errors = decode_errors
self.default_encoding = default_encoding
self.escape = escape
self.load_template = load_template
self.loader = loader
self.output_encoding = output_encoding
def _unicode_and_escape(self, s):
......@@ -139,11 +161,11 @@ class Renderer(object):
"""
def load_partial(name):
template = self.load_template(name)
# Make sure the return value of load_template is unicode since
# RenderEngine requires it. Also, check that the string is not
# already unicode to avoid "double-decoding". Otherwise, we
# would get the following error:
template = self.loader.get(name)
# Make sure the return value is unicode since RenderEngine requires
# it. Also, check that the string is not already unicode to
# avoid "double-decoding". Otherwise, we would get the following
# error:
# TypeError: decoding Unicode is not supported
if not isinstance(template, unicode):
template = self.unicode(template)
......@@ -168,19 +190,19 @@ class Renderer(object):
"""
Render the given template using the given context.
The return value is a unicode string, unless the output_encoding
attribute has been set to a non-None value, in which case the
return value has type str and is encoded using that encoding.
Returns:
If the template string is not unicode, it is first converted to
unicode using the default_encoding and decode_errors attributes.
See the Template constructor's docstring for more information.
If the output_encoding attribute is None, the return value is
a unicode string. Otherwise, the return value is encoded to a
string of type str using the output encoding named by the
output_encoding attribute.
Arguments:
template: a template string that is either unicode, or of type
str and encoded using the encoding named by the default_encoding
keyword argument.
template: a template string that is either unicode or of type str.
If the string has type str, it is first converted to unicode
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).
......
......@@ -21,16 +21,24 @@ class View(object):
template_encoding = None
template_extension = None
# A function that accepts a single template_name parameter.
_load_template = None
_loader = 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.
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:
self._load_template = load_template
if loader is not None:
self._loader = loader
if template is not None:
self.template = template
......@@ -43,17 +51,21 @@ class View(object):
self.context = _context
def load_template(self, template_name):
if self._load_template is None:
# We delay setting self._load_template until now (in the case
# that the user did not supply a load_template to the constructor)
def get_loader(self):
if self._loader is None:
# We delay setting self._loader until now (in the case that the
# user did not supply a load_template to the constructor)
# to let users set the template_extension attribute, etc. after
# View.__init__() has already been called.
loader = Loader(search_dirs=self.template_path, encoding=self.template_encoding,
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):
"""
......@@ -98,8 +110,9 @@ class View(object):
Return the view rendered using the current context.
"""
loader = self.get_loader()
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)
def get(self, key, default=None):
......
......@@ -48,36 +48,36 @@ class LoaderTestCase(unittest.TestCase):
loader.template_extension = ''
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')
template = loader.load_template('simple')
template = loader.get('simple')
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'])
template = loader.load_template('simple')
template = loader.get('simple')
self.assertEqual(template, 'Hi {{thing}}!{{blank}}')
def test_non_existent_template_fails(self):
def test_get__non_existent_template_fails(self):
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)
self.assertRaises(IOError, loader.load_template, 'extensionless')
self.assertRaises(IOError, loader.get, 'extensionless')
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.
"""
loader = Loader(search_dirs=self.search_dirs)
template = loader.load_template('simple')
template = loader.get('simple')
self.assertEqual(type(template), unicode)
......@@ -11,6 +11,37 @@ import unittest
from pystache 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):
......@@ -240,19 +271,37 @@ class RendererTestCase(unittest.TestCase):
renderer.default_encoding = 'utf_8'
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):
"""
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()
# 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()
self.assertEquals(load_partial("partial"), "foo")
# This would raise a TypeError exception if we tried to double-decode.
self.assertEquals(load_partial("test"), "partial")
# Now with a value that is already unicode.
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
# correctly, we no longer need to test the rendering code paths through
......@@ -263,14 +312,13 @@ class RendererTestCase(unittest.TestCase):
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.
# In real-life, the partial would be different with each name.
renderer.load_template = lambda name: "partial"
engine = renderer._make_render_engine()
# 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):
"""
......
......@@ -60,15 +60,13 @@ class ViewTestCase(unittest.TestCase):
template = view.load_template('extensionless')
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"}
load_template = lambda template_name: partials_dict[template_name]
view = Simple(load_template=load_template)
partials = {"partial": "Loaded from dictionary"}
view = Simple(loader=partials)
actual = view.load_template("partial")
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