Commit aa8ad43d by Chris Jerdonek

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

parents f5c8a965 383acd31
# coding: utf-8 # 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 ...@@ -13,42 +13,14 @@ import sys
DEFAULT_EXTENSION = 'mustache' 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): class Locator(object):
def __init__(self, search_dirs=None, extension=None): def __init__(self, extension=None):
""" """
Construct a template locator. Construct a template locator.
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". extension: the template file extension. Defaults to "mustache".
Pass False for no extension (i.e. extensionless template files). Pass False for no extension (i.e. extensionless template files).
...@@ -56,14 +28,35 @@ class Locator(object): ...@@ -56,14 +28,35 @@ class Locator(object):
if extension is None: if extension is None:
extension = DEFAULT_EXTENSION extension = DEFAULT_EXTENSION
if search_dirs is None: self.template_extension = extension
search_dirs = os.curdir # i.e. "."
def _find_path(self, file_name, search_dirs):
"""
Search for the given file, and return the path.
if isinstance(search_dirs, basestring): Returns None if the file is not found.
search_dirs = [search_dirs]
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): def make_file_name(self, template_name):
file_name = template_name file_name = template_name
...@@ -72,22 +65,41 @@ class Locator(object): ...@@ -72,22 +65,41 @@ class Locator(object):
return file_name 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) file_name = self.make_file_name(template_name)
path = self._find_path(file_name, search_dirs)
for dir_path in search_dirs: if path is not None:
file_path = os.path.join(dir_path, file_name) return path
if os.path.exists(file_path):
return file_path
# TODO: we should probably raise an exception of our own type. # 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): ...@@ -174,12 +174,12 @@ class Renderer(object):
""" """
return Reader(encoding=self.file_encoding, decode_errors=self.decode_errors) 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. 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): def _make_load_template(self):
""" """
...@@ -187,10 +187,10 @@ class Renderer(object): ...@@ -187,10 +187,10 @@ class Renderer(object):
""" """
reader = self._make_reader() reader = self._make_reader()
locator = self._make_locator() locator = self.make_locator()
def load_template(template_name): 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 reader.read(path)
return load_template return load_template
...@@ -255,6 +255,48 @@ class Renderer(object): ...@@ -255,6 +255,48 @@ class Renderer(object):
load_template = self._make_load_template() load_template = self._make_load_template()
return load_template(template_name) 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): def render_path(self, template_path, *context, **kwargs):
""" """
Render the template at the given path using the given context. Render the template at the given path using the given context.
...@@ -263,20 +305,27 @@ class Renderer(object): ...@@ -263,20 +305,27 @@ class Renderer(object):
""" """
template = self.read(template_path) template = self.read(template_path)
return self.render(template, *context, **kwargs)
return self._render_string(template, *context, **kwargs)
def render(self, 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: Arguments:
template: a template string that is either unicode or of type str. template: a template string of type unicode or str, or an object
If the string has type str, it is first converted to unicode instance. If the argument is an object, for the template string
using this instance's default_encoding and decode_errors the function attempts to find a template associated to the
attributes. See the constructor docstring for more information. 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 *context: zero or more dictionaries, Context instances, or objects
with which to populate the initial context stack. None with which to populate the initial context stack. None
...@@ -290,12 +339,8 @@ class Renderer(object): ...@@ -290,12 +339,8 @@ class Renderer(object):
all items in the *context list. all items in the *context list.
""" """
engine = self._make_render_engine() if not isinstance(template, basestring):
context = Context.create(*context, **kwargs) # Then we assume the template is an object.
return self._render_object(template, *context, **kwargs)
# RenderEngine.render() requires that the template string be unicode.
template = self._to_unicode_hard(template)
rendered = engine.render(template, context)
return unicode(rendered) return self._render_string(template, *context, **kwargs)
...@@ -6,7 +6,7 @@ This module provides a View class. ...@@ -6,7 +6,7 @@ This module provides a View class.
""" """
from .context import Context from .context import Context
from .locator import make_template_name from .locator import Locator
from .renderer import Renderer from .renderer import Renderer
...@@ -20,6 +20,8 @@ class View(object): ...@@ -20,6 +20,8 @@ class View(object):
_renderer = None _renderer = None
locator = Locator()
def __init__(self, template=None, context=None, partials=None, **kwargs): def __init__(self, template=None, context=None, partials=None, **kwargs):
""" """
Construct a View instance. Construct a View instance.
...@@ -85,7 +87,7 @@ class View(object): ...@@ -85,7 +87,7 @@ class View(object):
if self.template_name: if self.template_name:
return self.template_name return self.template_name
return make_template_name(self) return self.locator.make_template_name(self)
def render(self): def render(self):
""" """
......
Hello {{to}} Hello, {{to}}
\ No newline at end of file \ 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): ...@@ -46,8 +46,9 @@ def buildTest(testData, spec_filename):
expected = testData['expected'] expected = testData['expected']
data = testData['data'] data = testData['data']
renderer = Renderer(loader=partials) renderer = Renderer(partials=partials)
actual = renderer.render(template, data).encode('utf-8') actual = renderer.render(template, data)
actual = actual.encode('utf-8')
message = """%s message = """%s
......
...@@ -9,27 +9,12 @@ import os ...@@ -9,27 +9,12 @@ import os
import sys import sys
import unittest import unittest
from pystache.locator import make_template_name
from pystache.locator import Locator from pystache.locator import Locator
from pystache.reader import Reader from pystache.reader import Reader
from .common import DATA_DIR 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): class LocatorTests(unittest.TestCase):
search_dirs = 'examples' search_dirs = 'examples'
...@@ -37,14 +22,6 @@ class LocatorTests(unittest.TestCase): ...@@ -37,14 +22,6 @@ class LocatorTests(unittest.TestCase):
def _locator(self): def _locator(self):
return Locator(search_dirs=DATA_DIR) 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): def test_init__extension(self):
# Test the default value. # Test the default value.
locator = Locator() locator = Locator()
...@@ -56,6 +33,16 @@ class LocatorTests(unittest.TestCase): ...@@ -56,6 +33,16 @@ class LocatorTests(unittest.TestCase):
locator = Locator(extension=False) locator = Locator(extension=False)
self.assertTrue(locator.template_extension is 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): def test_make_file_name(self):
locator = Locator() locator = Locator()
...@@ -69,14 +56,14 @@ class LocatorTests(unittest.TestCase): ...@@ -69,14 +56,14 @@ class LocatorTests(unittest.TestCase):
self.assertEquals(locator.make_file_name('foo'), 'foo.') self.assertEquals(locator.make_file_name('foo'), 'foo.')
def test_locate_path(self): def test_locate_path(self):
locator = Locator(search_dirs='examples') locator = Locator()
path = locator.locate_path('simple') path = locator.locate_path('simple', search_dirs=['examples'])
self.assertEquals(os.path.basename(path), 'simple.mustache') self.assertEquals(os.path.basename(path), 'simple.mustache')
def test_locate_path__using_list_of_paths(self): def test_locate_path__using_list_of_paths(self):
locator = Locator(search_dirs=['doesnt_exist', 'examples']) locator = Locator()
path = locator.locate_path('simple') path = locator.locate_path('simple', search_dirs=['doesnt_exist', 'examples'])
self.assertTrue(path) self.assertTrue(path)
...@@ -90,13 +77,10 @@ class LocatorTests(unittest.TestCase): ...@@ -90,13 +77,10 @@ class LocatorTests(unittest.TestCase):
dir1 = DATA_DIR dir1 = DATA_DIR
dir2 = os.path.join(DATA_DIR, 'locator') dir2 = os.path.join(DATA_DIR, 'locator')
locator.search_dirs = [dir1] self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir1]))
self.assertTrue(locator.locate_path('duplicate')) self.assertTrue(locator.locate_path('duplicate', search_dirs=[dir2]))
locator.search_dirs = [dir2]
self.assertTrue(locator.locate_path('duplicate'))
locator.search_dirs = [dir2, dir1] path = locator.locate_path('duplicate', search_dirs=[dir2, dir1])
path = locator.locate_path('duplicate')
dirpath = os.path.dirname(path) dirpath = os.path.dirname(path)
dirname = os.path.split(dirpath)[-1] dirname = os.path.split(dirpath)[-1]
...@@ -105,5 +89,17 @@ class LocatorTests(unittest.TestCase): ...@@ -105,5 +89,17 @@ class LocatorTests(unittest.TestCase):
def test_locate_path__non_existent_template_fails(self): def test_locate_path__non_existent_template_fails(self):
locator = Locator() 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 ...@@ -15,6 +15,8 @@ from pystache.renderer import Renderer
from pystache.locator import Locator from pystache.locator import Locator
from .common import get_data_path from .common import get_data_path
from .data.templates import SayHello
class RendererInitTestCase(unittest.TestCase): class RendererInitTestCase(unittest.TestCase):
...@@ -216,53 +218,40 @@ class RendererTestCase(unittest.TestCase): ...@@ -216,53 +218,40 @@ class RendererTestCase(unittest.TestCase):
actual = self._read(renderer, filename) actual = self._read(renderer, filename)
self.assertEquals(actual, 'non-ascii: ') 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() renderer = Renderer()
locator = renderer._make_locator() locator = renderer.make_locator()
self.assertEquals(type(locator), 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 = Renderer()
renderer.file_extension = 'foo' renderer.file_extension = 'foo'
locator = renderer._make_locator() locator = renderer.make_locator()
self.assertEquals(locator.template_extension, 'foo') 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 # This test is a sanity check. Strictly speaking, it shouldn't
# be necessary based on our tests above. # be necessary based on our tests above.
def test__make_locator__default(self): def test_make_locator__default(self):
renderer = Renderer() renderer = Renderer()
actual = renderer._make_locator() actual = renderer.make_locator()
expected = Locator() expected = Locator()
self.assertEquals(type(actual), type(expected)) self.assertEquals(type(actual), type(expected))
self.assertEquals(actual.template_extension, expected.template_extension) self.assertEquals(actual.template_extension, expected.template_extension)
self.assertEquals(actual.search_dirs, expected.search_dirs)
## Test the render() method. ## Test the render() method.
...@@ -388,9 +377,22 @@ class RendererTestCase(unittest.TestCase): ...@@ -388,9 +377,22 @@ class RendererTestCase(unittest.TestCase):
""" """
renderer = Renderer() renderer = Renderer()
path = get_data_path('say_hello.mustache') path = get_data_path('say_hello.mustache')
actual = renderer.render_path(path, to='world') actual = renderer.render_path(path, to='foo')
self.assertEquals(actual, "Hello world") 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, # By testing that Renderer.render() constructs the right RenderEngine,
# we no longer need to exercise all rendering code paths through # 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