diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | coverage/templite.py | 81 | ||||
-rw-r--r-- | test/test_templite.py | 95 |
3 files changed, 177 insertions, 1 deletions
@@ -20,7 +20,7 @@ clean: -rm -f $(TEST_ZIP) -rm -f setuptools-*.egg -LINTABLE_TESTS = test/test_data.py test/test_execfile.py test/test_farm.py test/coveragetest.py +LINTABLE_TESTS = test/test_data.py test/test_execfile.py test/test_farm.py test/coveragetest.py test/test_templite.py lint: clean -python -x /Python25/Scripts/pylint.bat --rcfile=.pylintrc coverage diff --git a/coverage/templite.py b/coverage/templite.py new file mode 100644 index 00000000..ec845076 --- /dev/null +++ b/coverage/templite.py @@ -0,0 +1,81 @@ +"""A simple Python template renderer, for a nano-subset of Django syntax.""" + +# Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html +# and http://jtauber.com/2006/05/templates.html +# and http://code.activestate.com/recipes/496730/ + +import re + +class Templite(object): + """A simple template renderer, for a nano-subset of Django syntax. + + """ + def __init__(self, text, *contexts): + self.loops = [] + self.text = self._prepare(text) + self.context = {} + for context in contexts: + self.context.update(context) + + def render(self, context=None): + # Make the complete context we'll use. + ctx = dict(self.context) + if context: + ctx.update(context) + + ctxaccess = ContextAccess(ctx) + + # Render the loops. + for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops): + result = "" + for listval in ctxaccess[listvar]: + ctx[loopvar] = listval + result += loopbody % ctxaccess + ctx["loop:%d" % iloop] = result + + # Render the final template. + return self.text % ctxaccess + + def _prepare(self, text): + """Convert Django-style data references into Python-native ones.""" + # Pull out loops. + text = re.sub( + r"{% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*){% endfor %}", + self._loop_repl, text + ) + # Protect actual percent signs in the text. + text = text.replace("%", "%%") + # Convert {{foo}} into %(foo)s + text = re.sub(r"{{([^}]+)}}", r"%(\1)s", text) + return text + + def _loop_repl(self, match): + nloop = len(self.loops) + # Append (loopvar, listvar, loopbody) to self.loops + loopvar, listvar, loopbody = match.groups() + loopbody = self._prepare(loopbody) + self.loops.append((loopvar, listvar, loopbody)) + return "{{loop:%d}}" % nloop + + +class ContextAccess(object): + + def __init__(self, context): + self.context = context + + def __getitem__(self, key): + if "|" in key: + pipes = key.split("|") + value = self[pipes[0]] + for func in pipes[1:]: + value = self[func](value) + elif "." in key: + dots = key.split('.') + value = self[dots[0]] + for dot in dots[1:]: + value = getattr(value, dot) + if callable(value): + value = value() + else: + value = self.context[key] + return value diff --git a/test/test_templite.py b/test/test_templite.py new file mode 100644 index 00000000..a270bf8e --- /dev/null +++ b/test/test_templite.py @@ -0,0 +1,95 @@ +"""Tests for coverage.template.""" + +from coverage.templite import Templite +import unittest + +class AnyOldObject: + pass + +class TemplateTest(unittest.TestCase): + + def test_passthrough(self): + # Strings without variables are passed through unchanged. + self.assertEqual(Templite("Hello").render(), "Hello") + self.assertEqual( + Templite("Hello, 20% fun time!").render(), + "Hello, 20% fun time!" + ) + + def test_variables(self): + # Variables use {{var}} syntax. + self.assertEqual( + Templite("Hello, {{name}}!").render({'name':'Ned'}), + "Hello, Ned!" + ) + + def test_pipes(self): + # Variables can be filtered with pipes. + data = { + 'name': 'Ned', + 'upper': lambda x: x.upper(), + 'second': lambda x: x[1], + } + self.assertEqual( + Templite("Hello, {{name|upper}}!").render(data), + "Hello, NED!" + ) + # Pipes can be concatenated. + self.assertEqual( + Templite("Hello, {{name|upper|second}}!").render(data), + "Hello, E!" + ) + + def test_reusability(self): + # A single Templite can be used more than once with different data. + globs = { + 'upper': lambda x: x.upper(), + 'punct': '!', + } + + template = Templite("This is {{name|upper}}{{punct}}", globs) + self.assertEqual(template.render({'name':'Ned'}), "This is NED!") + self.assertEqual(template.render({'name':'Ben'}), "This is BEN!") + + def test_attribute(self): + # Variables' attributes can be accessed with dots. + obj = AnyOldObject() + obj.a = "Ay" + self.assertEqual( + Templite("{{obj.a}}").render(locals()), "Ay" + ) + + obj2 = AnyOldObject() + obj2.obj = obj + obj2.b = "Bee" + self.assertEqual( + Templite("{{obj2.obj.a}} {{obj2.b}}").render(locals()), "Ay Bee" + ) + + def test_member_function(self): + # Variables' member functions can be used, as long as they are nullary. + class WithMemberFns: + def ditto(self): + return self.txt + self.txt + obj = WithMemberFns() + obj.txt = "Once" + self.assertEqual( + Templite("{{obj.ditto}}").render(locals()), "OnceOnce" + ) + + def test_loops(self): + # Loops work like in Django. + nums = [1,2,3,4] + self.assertEqual( + Templite("Look: {% for n in nums %}{{n}}, {% endfor %}done."). + render(locals()), "Look: 1, 2, 3, 4, done." + ) + rev = reversed + self.assertEqual( + Templite("Look: {% for n in nums|rev %}{{n}}, {% endfor %}done."). + render(locals()), "Look: 4, 3, 2, 1, done." + ) + + +if __name__ == '__main__': + unittest.main() |