Commit aa8ad43d by Chris Jerdonek

Merge branch 'issue_70' into development: closing issue #70

parents f5c8a965 383acd31
# coding: utf-8
"""
This module provides a Locator class.
This module provides a Locator class for finding template files.
"""
......@@ -13,42 +13,14 @@ import sys
DEFAULT_EXTENSION = 'mustache'
def make_template_name(obj):
"""
Return the canonical template name for an object instance.
This method converts Python-style class names (PEP 8's recommended
CamelCase, aka CapWords) to lower_case_with_underscords. Here
is an example with code:
>>> class HelloWorld(object):
... pass
>>> hi = HelloWorld()
>>> make_template_name(hi)
'hello_world'
"""
template_name = obj.__class__.__name__
def repl(match):
return '_' + match.group(0).lower()
return re.sub('[A-Z]', repl, template_name)[1:]
class Locator(object):
def __init__(self, search_dirs=None, extension=None):
def __init__(self, extension=None):
"""
Construct a template locator.
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).
......@@ -56,14 +28,35 @@ class Locator(object):
if extension is None:
extension = DEFAULT_EXTENSION
if search_dirs is None:
search_dirs = os.curdir # i.e. "."
self.template_extension = extension
if isinstance(search_dirs, basestring):
search_dirs = [search_dirs]
def _find_path(self, file_name, search_dirs):
"""
Search for the given file, and return the path.
self.search_dirs = search_dirs
self.template_extension = extension
Returns None if the file is not found.
"""
for dir_path in search_dirs:
file_path = os.path.join(dir_path, file_name)
if os.path.exists(file_path):
return file_path
return None
def get_object_directory(self, obj):
"""
Return the directory containing an object's defining class.
"""
module = sys.modules[obj.__module__]
# TODO: should we handle the case of __file__ not existing, for
# example when using the interpreter or using a module in the
# standard library)?
path = module.__file__
return os.path.dirname(path)
def make_file_name(self, template_name):
file_name = template_name
......@@ -72,22 +65,41 @@ class Locator(object):
return file_name
def locate_path(self, template_name):
def make_template_name(self, obj):
"""
Find and return the path to the template with the given name.
Return the canonical template name for an object instance.
This method converts Python-style class names (PEP 8's recommended
CamelCase, aka CapWords) to lower_case_with_underscords. Here
is an example with code:
>>> class HelloWorld(object):
... pass
>>> hi = HelloWorld()
>>>
>>> locator = Locator()
>>> locator.make_template_name(hi)
'hello_world'
"""
template_name = obj.__class__.__name__
def repl(match):
return '_' + match.group(0).lower()
Raises an IOError if the template cannot be found.
return re.sub('[A-Z]', repl, template_name)[1:]
def locate_path(self, template_name, search_dirs):
"""
search_dirs = self.search_dirs
Find and return the path to the template with the given name.
"""
file_name = self.make_file_name(template_name)
path = self._find_path(file_name, search_dirs)
for dir_path in search_dirs:
file_path = os.path.join(dir_path, file_name)
if os.path.exists(file_path):
return file_path
if path is not None:
return path
# TODO: we should probably raise an exception of our own type.
raise IOError('"%s" not found in "%s"' % (template_name, ':'.join(search_dirs),))
raise IOError('Template %s not found in directories: %s' %
(repr(template_name), repr(search_dirs)))
......@@ -174,12 +174,12 @@ class Renderer(object):
"""
return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors)
def _make_locator(self):
def make_locator(self):
"""
Create a Locator instance using current attributes.
"""
return Locator(search_dirs=self.search_dirs, extension=self.file_extension)
return Locator(extension=self.file_extension)
def _make_load_template(self):
"""
......@@ -187,10 +187,10 @@ class Renderer(object):
"""
reader = self._make_reader()
locator = self._make_locator()
locator = self.make_locator()
def load_template(template_name):
path = locator.locate_path(template_name)
path = locator.locate_path(template_name=template_name, search_dirs=self.search_dirs)
return reader.read(path)
return load_template
......@@ -255,6 +255,48 @@ class Renderer(object):
load_template = self._make_load_template()
return load_template(template_name)
def get_associated_template(self, obj):
"""
Find and return the template associated with an object.
The function first searches the directory containing the object's
class definition.
"""
locator = self.make_locator()
template_name = locator.make_template_name(obj)
directory = locator.get_object_directory(obj)
search_dirs = [directory] + self.search_dirs
path = locator.locate_path(template_name=template_name, search_dirs=search_dirs)
return self.read(path)
def _render_string(self, template, *context, **kwargs):
"""
Render the given template string using the given context.
"""
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
context = Context.create(*context, **kwargs)
engine = self._make_render_engine()
rendered = engine.render(template, context)
return unicode(rendered)
def _render_object(self, obj, *context, **kwargs):
"""
Render the template associated with the given object.
"""
context = [obj] + list(context)
template = self.get_associated_template(obj)
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.
......@@ -263,20 +305,27 @@ class Renderer(object):
"""
template = self.read(template_path)
return self.render(template, *context, **kwargs)
return self._render_string(template, *context, **kwargs)
def render(self, template, *context, **kwargs):
"""
Render the given template using the given context.
Render the given template (or templated object) using the given context.
Returns the rendering as a unicode string.
Returns a unicode string.
Prior to rendering, templates of type str are converted to unicode
using the default_encoding and decode_errors attributes. See the
constructor docstring for more information.
Arguments:
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 this instance's default_encoding and decode_errors
attributes. See the constructor docstring for more information.
template: a template string of type unicode or str, or an object
instance. If the argument is an object, for the template string
the function attempts to find a template associated to the
object by calling the get_associated_template() method. The
argument in this case is also used as the first element of the
context stack when rendering the associated template.
*context: zero or more dictionaries, Context instances, or objects
with which to populate the initial context stack. None
......@@ -290,12 +339,8 @@ class Renderer(object):
all items in the *context list.
"""
engine = self._make_render_engine()
context = Context.create(*context, **kwargs)
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
rendered = engine.render(template, context)
if not isinstance(template, basestring):
# Then we assume the template is an object.
return self._render_object(template, *context, **kwargs)
return unicode(rendered)
return self._render_string(template, *context, **kwargs)
......@@ -6,7 +6,7 @@ This module provides a View class.
"""
from .context import Context
from .locator import make_template_name
from .locator import Locator
from .renderer import Renderer
......@@ -20,6 +20,8 @@ class View(object):
_renderer = None
locator = Locator()
def __init__(self, template=None, context=None, partials=None, **kwargs):
"""
Construct a View instance.
......@@ -85,7 +87,7 @@ class View(object):
if self.template_name:
return self.template_name
return make_template_name(self)
return self.locator.make_template_name(self)
def render(self):
"""
......
Hello {{to}}
\ No newline at end of file
Hello, {{to}}
\ No newline at end of file
# coding: utf-8
class SayHello(object):
def to(self):
return "World"
......@@ -46,8 +46,9 @@ def buildTest(testData, spec_filename):
expected = testData['expected']
data = testData['data']
renderer = Renderer(loader=partials)
actual = renderer.render(template, data).encode('utf-8')
renderer = Renderer(partials=partials)
actual = renderer.render(template, data)
actual = actual.encode('utf-8')
message = """%s
......
......@@ -9,27 +9,12 @@ import os
import sys
import unittest
from pystache.locator import make_template_name
from pystache.locator import Locator
from pystache.reader import Reader
from .common import DATA_DIR
class MakeTemplateNameTests(unittest.TestCase):
"""
Test the make_template_name() function.
"""
def test(self):
class FooBar(object):
pass
foo = FooBar()
self.assertEquals(make_template_name(foo), 'foo_bar')
class LocatorTests(unittest.TestCase):
search_dirs = 'examples'
......@@ -37,14 +22,6 @@ class LocatorTests(unittest.TestCase):
def _locator(self):
return Locator(search_dirs=DATA_DIR)
def test_init__search_dirs(self):
# Test the default value.
locator = Locator()
self.assertEquals(locator.search_dirs, [os.curdir])
locator = Locator(search_dirs=['foo'])
self.assertEquals(locator.search_dirs, ['foo'])
def test_init__extension(self):
# Test the default value.
locator = Locator()
......@@ -56,6 +33,16 @@ class LocatorTests(unittest.TestCase):
locator = Locator(extension=False)
self.assertTrue(locator.template_extension is False)
def test_get_object_directory(self):
locator = Locator()
reader = Reader()
actual = locator.get_object_directory(reader)
expected = os.path.join(os.path.dirname(__file__), os.pardir, 'pystache')
self.assertEquals(os.path.normpath(actual), os.path.normpath(expected))
def test_make_file_name(self):
locator = Locator()
......@@ -69,14 +56,14 @@ class LocatorTests(unittest.TestCase):
self.assertEquals(locator.make_file_name('foo'), 'foo.')
def test_locate_path(self):
locator = Locator(search_dirs='examples')
path = locator.locate_path('simple')
locator = Locator()
path = locator.locate_path('simple', search_dirs=['examples'])
self.assertEquals(os.path.basename(path), 'simple.mustache')
def test_locate_path__using_list_of_paths(self):
locator = Locator(search_dirs=['doesnt_exist', 'examples'])
path = locator.locate_path('simple')
locator = Locator()
path = locator.locate_path('simple', search_dirs=['doesnt_exist', 'examples'])
self.assertTrue(path)
......@@ -90,13 +77,10 @@ class LocatorTests(unittest.TestCase):
dir1 = DATA_DIR
dir2 = os.path.join(DATA_DIR, 'locator')
locator.search_dirs = [dir1]
self.assertTrue(locator.locate_path('duplicate'))
locator.search_dirs = [dir2]
self.assertTrue(locator.locate_path('duplicate'))
self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir1]))
self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir2]))
locator.search_dirs = [dir2, dir1]
path = locator.locate_path('duplicate')
path = locator.locate_path('duplicate', search_dirs=[dir2, dir1])
dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1]
......@@ -105,5 +89,17 @@ class LocatorTests(unittest.TestCase):
def test_locate_path__non_existent_template_fails(self):
locator = Locator()
self.assertRaises(IOError, locator.locate_path, 'doesnt_exist')
self.assertRaises(IOError, locator.locate_path, 'doesnt_exist', search_dirs=[])
def test_make_template_name(self):
"""
Test make_template_name().
"""
locator = Locator()
class FooBar(object):
pass
foo = FooBar()
self.assertEquals(locator.make_template_name(foo), 'foo_bar')
......@@ -15,6 +15,8 @@ from pystache.renderer import Renderer
from pystache.locator import Locator
from .common import get_data_path
from .data.templates import SayHello
class RendererInitTestCase(unittest.TestCase):
......@@ -216,53 +218,40 @@ class RendererTestCase(unittest.TestCase):
actual = self._read(renderer, filename)
self.assertEquals(actual, 'non-ascii: ')
## Test the _make_locator() method.
## Test the make_locator() method.
def test__make_locator__return_type(self):
def test_make_locator__return_type(self):
"""
Test that _make_locator() returns a Locator.
Test that make_locator() returns a Locator.
"""
renderer = Renderer()
locator = renderer._make_locator()
locator = renderer.make_locator()
self.assertEquals(type(locator), Locator)
def test__make_locator__file_extension(self):
def test_make_locator__file_extension(self):
"""
Test that _make_locator() respects the file_extension attribute.
Test that make_locator() respects the file_extension attribute.
"""
renderer = Renderer()
renderer.file_extension = 'foo'
locator = renderer._make_locator()
locator = renderer.make_locator()
self.assertEquals(locator.template_extension, 'foo')
def test__make_locator__search_dirs(self):
"""
Test that _make_locator() respects the search_dirs attribute.
"""
renderer = Renderer()
renderer.search_dirs = ['foo']
locator = renderer._make_locator()
self.assertEquals(locator.search_dirs, ['foo'])
# This test is a sanity check. Strictly speaking, it shouldn't
# be necessary based on our tests above.
def test__make_locator__default(self):
def test_make_locator__default(self):
renderer = Renderer()
actual = renderer._make_locator()
actual = renderer.make_locator()
expected = Locator()
self.assertEquals(type(actual), type(expected))
self.assertEquals(actual.template_extension, expected.template_extension)
self.assertEquals(actual.search_dirs, expected.search_dirs)
## Test the render() method.
......@@ -388,9 +377,22 @@ class RendererTestCase(unittest.TestCase):
"""
renderer = Renderer()
path = get_data_path('say_hello.mustache')
actual = renderer.render_path(path, to='world')
self.assertEquals(actual, "Hello world")
actual = renderer.render_path(path, to='foo')
self.assertEquals(actual, "Hello, foo")
def test_render__object(self):
"""
Test rendering an object instance.
"""
renderer = Renderer()
say_hello = SayHello()
actual = renderer.render(say_hello)
self.assertEquals('Hello, World', actual)
actual = renderer.render(say_hello, to='Mars')
self.assertEquals('Hello, Mars', actual)
# By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through
......
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