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
def _find_path(self, file_name, search_dirs):
"""
Search for the given file, and return the path.
if isinstance(search_dirs, basestring):
search_dirs = [search_dirs]
Returns None if the file is not found.
self.search_dirs = search_dirs
self.template_extension = extension
"""
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.
Raises an IOError if the template cannot be found.
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'
"""
search_dirs = self.search_dirs
template_name = obj.__class__.__name__
def repl(match):
return '_' + match.group(0).lower()
return re.sub('[A-Z]', repl, template_name)[1:]
def locate_path(self, template_name, 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