Commit 98e4f43f by Chris Jerdonek

The View class now does all its loading through a Renderer instance.

This will help with future refactoring and code maintenance by not
having the same logic duplicated in more than one part of the code.
It also takes more responsibility away from the View class, which
is what we want.
parent 4720fa7f
...@@ -6,9 +6,11 @@ This module provides a Renderer class to render templates. ...@@ -6,9 +6,11 @@ This module provides a Renderer class to render templates.
""" """
import cgi import cgi
import os
import sys import sys
from .context import Context from .context import Context
from .loader import DEFAULT_EXTENSION
from .loader import Loader from .loader import Loader
from .reader import Reader from .reader import Reader
from .renderengine import RenderEngine from .renderengine import RenderEngine
...@@ -34,7 +36,8 @@ class Renderer(object): ...@@ -34,7 +36,8 @@ class Renderer(object):
""" """
def __init__(self, loader=None, file_encoding=None, default_encoding=None, def __init__(self, loader=None, file_encoding=None, default_encoding=None,
decode_errors='strict', escape=None): decode_errors='strict', search_dirs=None, file_extension=None,
escape=None):
""" """
Construct an instance. Construct an instance.
...@@ -80,28 +83,54 @@ class Renderer(object): ...@@ -80,28 +83,54 @@ class Renderer(object):
strings of type str encountered during the rendering process. strings of type str encountered during the rendering process.
Defaults to "strict". Defaults to "strict".
search_dirs: the list of directories in which to search for
templates when loading a template by name. Defaults to the
current working directory. If given a string, the string is
interpreted as a single directory.
file_extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. for extensionless files).
""" """
if default_encoding is None: if default_encoding is None:
default_encoding = sys.getdefaultencoding() default_encoding = sys.getdefaultencoding()
if file_encoding is None:
file_encoding = default_encoding
if escape is None: if escape is None:
# The quote=True argument causes double quotes to be escaped, # The quote=True argument causes double quotes to be escaped,
# but not single quotes: # but not single quotes:
# http://docs.python.org/library/cgi.html#cgi.escape # http://docs.python.org/library/cgi.html#cgi.escape
escape = lambda s: cgi.escape(s, quote=True) escape = lambda s: cgi.escape(s, quote=True)
# This needs to be after we set the default default_encoding.
if file_encoding is None:
file_encoding = default_encoding
if file_extension is None:
file_extension = DEFAULT_EXTENSION
if search_dirs is None:
search_dirs = os.curdir # i.e. "."
if isinstance(search_dirs, basestring):
search_dirs = [search_dirs]
# This needs to be after we set some of the defaults above.
if loader is None: if loader is None:
reader = Reader(encoding=file_encoding, decode_errors=decode_errors) reader = Reader(encoding=file_encoding, decode_errors=decode_errors)
loader = Loader(reader=reader) loader = Loader(reader=reader, search_dirs=search_dirs, extension=file_extension)
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.file_encoding = file_encoding self.file_encoding = file_encoding
self.file_extension = file_extension
# TODO: we should not store a loader attribute because the loader
# would no longer reflect the current attributes if, say, someone
# changed the search_dirs attribute after instantiation. Instead,
# we should construct the Loader instance each time on the fly,
# as we do with the Reader in the read() method.
self.loader = loader self.loader = loader
self.search_dirs = search_dirs
def _to_unicode_soft(self, s): def _to_unicode_soft(self, s):
""" """
......
...@@ -23,6 +23,7 @@ class View(object): ...@@ -23,6 +23,7 @@ class View(object):
template_extension = None template_extension = None
_loader = None _loader = None
_renderer = None
def __init__(self, template=None, context=None, loader=None, **kwargs): def __init__(self, template=None, context=None, loader=None, **kwargs):
""" """
...@@ -52,22 +53,20 @@ class View(object): ...@@ -52,22 +53,20 @@ class View(object):
self.context = _context self.context = _context
def get_loader(self): def _get_renderer(self):
if self._loader is None: if self._renderer is None:
# We delay setting self._loader until now (in the case that the # We delay setting self._renderer until now (instead of, say,
# user did not supply a load_template to the constructor) # setting it in the constructor) in case the user changes after
# to let users set the template_extension attribute, etc. after # instantiation some of the attributes on which the Renderer
# View.__init__() has already been called. # depends. This lets users set the template_extension attribute,
reader = Reader(encoding=self.template_encoding) # etc. after View.__init__() has already been called.
loader = Loader(search_dirs=self.template_path, reader=reader, renderer = Renderer(loader=self._loader,
extension=self.template_extension) file_encoding=self.template_encoding,
self._loader = loader search_dirs=self.template_path,
file_extension=self.template_extension)
return self._loader self._renderer = renderer
def load_template(self, template_name): return self._renderer
loader = self.get_loader()
return loader.get(template_name)
def get_template(self): def get_template(self):
""" """
...@@ -76,7 +75,8 @@ class View(object): ...@@ -76,7 +75,8 @@ class View(object):
""" """
if not self.template: if not self.template:
template_name = self._get_template_name() template_name = self._get_template_name()
self.template = self.load_template(template_name) renderer = self._get_renderer()
self.template = renderer.loader.get(template_name)
return self.template return self.template
...@@ -102,19 +102,13 @@ class View(object): ...@@ -102,19 +102,13 @@ class View(object):
return re.sub('[A-Z]', repl, template_name)[1:] return re.sub('[A-Z]', repl, template_name)[1:]
# TODO: the View class should probably have some sort of template renderer def render(self):
# associated with it to encapsulate all of the render-specific behavior
# and options like encoding, escape, etc. This would probably be better
# than passing all of these options to render(), especially as the list
# of possible options grows.
def render(self, escape=None):
""" """
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(escape=escape, loader=loader) renderer = self._get_renderer()
return renderer.render(template, self.context) return renderer.render(template, self.context)
def get(self, key, default=None): def get(self, key, default=None):
......
...@@ -31,10 +31,6 @@ class TestView(unittest.TestCase): ...@@ -31,10 +31,6 @@ class TestView(unittest.TestCase):
def test_escaping(self): def test_escaping(self):
self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>") self.assertEquals(Escaped().render(), "<h1>Bear &gt; Shark</h1>")
def test_escaping__custom(self):
escape = lambda s: s.upper()
self.assertEquals(Escaped().render(escape=escape), "<h1>BEAR > SHARK</h1>")
def test_literal(self): def test_literal(self):
self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>") self.assertEquals(Unescaped().render(), "<h1>Bear > Shark</h1>")
......
...@@ -6,6 +6,7 @@ Unit tests of template.py. ...@@ -6,6 +6,7 @@ Unit tests of template.py.
""" """
import codecs import codecs
import os
import sys import sys
import unittest import unittest
...@@ -66,6 +67,26 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -66,6 +67,26 @@ class RendererInitTestCase(unittest.TestCase):
self.assertEquals(reader.decode_errors, 'foo') self.assertEquals(reader.decode_errors, 'foo')
def test_loader__default__file_extension(self):
"""
Test that the default loader inherits file_extension.
"""
renderer = Renderer(file_extension='foo')
loader = renderer.loader
self.assertEquals(loader.template_extension, 'foo')
def test_loader__default__search_dirs(self):
"""
Test that the default loader inherits search_dirs.
"""
renderer = Renderer(search_dirs='foo')
loader = renderer.loader
self.assertEquals(loader.search_dirs, ['foo'])
def test_escape__default(self): def test_escape__default(self):
escape = Renderer().escape escape = Renderer().escape
...@@ -113,7 +134,7 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -113,7 +134,7 @@ class RendererInitTestCase(unittest.TestCase):
def test_file_encoding__default(self): def test_file_encoding__default(self):
""" """
Check that file_encoding defaults to default_encoding. Check the file_encoding default.
""" """
renderer = Renderer() renderer = Renderer()
...@@ -127,6 +148,46 @@ class RendererInitTestCase(unittest.TestCase): ...@@ -127,6 +148,46 @@ class RendererInitTestCase(unittest.TestCase):
renderer = Renderer(file_encoding='foo') renderer = Renderer(file_encoding='foo')
self.assertEquals(renderer.file_encoding, 'foo') self.assertEquals(renderer.file_encoding, 'foo')
def test_file_extension__default(self):
"""
Check the file_extension default.
"""
renderer = Renderer()
self.assertEquals(renderer.file_extension, 'mustache')
def test_file_extension(self):
"""
Check that the file_encoding attribute is set correctly.
"""
renderer = Renderer(file_extension='foo')
self.assertEquals(renderer.file_extension, 'foo')
def test_search_dirs__default(self):
"""
Check the search_dirs default.
"""
renderer = Renderer()
self.assertEquals(renderer.search_dirs, [os.curdir])
def test_search_dirs__string(self):
"""
Check that the search_dirs attribute is set correctly when a string.
"""
renderer = Renderer(search_dirs='foo')
self.assertEquals(renderer.search_dirs, ['foo'])
def test_search_dirs__list(self):
"""
Check that the search_dirs attribute is set correctly when a list.
"""
renderer = Renderer(search_dirs=['foo'])
self.assertEquals(renderer.search_dirs, ['foo'])
class RendererTestCase(unittest.TestCase): class RendererTestCase(unittest.TestCase):
......
...@@ -46,29 +46,16 @@ class ViewTestCase(unittest.TestCase): ...@@ -46,29 +46,16 @@ class ViewTestCase(unittest.TestCase):
view = Simple(thing='world') view = Simple(thing='world')
self.assertEquals(view.render(), "Hi world!") self.assertEquals(view.render(), "Hi world!")
def test_load_template(self):
"""
Test View.load_template().
"""
template = Simple().load_template("escaped")
self.assertEquals(template, "<h1>{{title}}</h1>")
def test_load_template__extensionless_file(self):
view = Simple()
view.template_extension = False
template = view.load_template('extensionless')
self.assertEquals(template, "No file extension: {{foo}}")
def test_load_template__custom_loader(self): def test_load_template__custom_loader(self):
""" """
Test passing a custom loader to View.__init__(). Test passing a custom loader to View.__init__().
""" """
template = "{{>partial}}"
partials = {"partial": "Loaded from dictionary"} partials = {"partial": "Loaded from dictionary"}
view = Simple(loader=partials) view = Simple(template=template, loader=partials)
actual = view.render()
actual = view.load_template("partial")
self.assertEquals(actual, "Loaded from dictionary") self.assertEquals(actual, "Loaded from dictionary")
def test_template_path(self): def test_template_path(self):
......
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