summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--coverage/templite.py81
-rw-r--r--test/test_templite.py95
3 files changed, 177 insertions, 1 deletions
diff --git a/Makefile b/Makefile
index 7b35892f..b191810d 100644
--- a/Makefile
+++ b/Makefile
@@ -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()