summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt2
-rw-r--r--TODO.txt1
-rw-r--r--coverage/templite.py194
-rw-r--r--tests/test_templite.py43
4 files changed, 142 insertions, 98 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 6de95124..5b53ec74 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -8,7 +8,7 @@ Change history for Coverage.py
- Fixed the mechanism for finding OS-installed static files for the HTML report
so that it will actually find OS-installed static files.
-- Improved the speed of HTML report generation by about 10%.
+- Improved the speed of HTML report generation by about 20%.
3.7 --- 6 October 2013
diff --git a/TODO.txt b/TODO.txt
index 173932b2..3a07a079 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -19,6 +19,7 @@ Key:
- collections.defaultdict
- .startswith((,))
- with assertRaises
+ - exec statement can look like a function in py2 (since when?)
- Remove code only run on <2.6
- Change data file to json
diff --git a/coverage/templite.py b/coverage/templite.py
index c39e061e..2ca12fef 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -2,7 +2,42 @@
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
-import re, sys
+import re
+
+
+class CodeBuilder(object):
+ """Build source code conveniently."""
+
+ def __init__(self):
+ self.code = []
+ self.indent_amount = 0
+
+ def add_line(self, line):
+ """Add a line of source to the code.
+
+ Don't include indentations or newlines.
+
+ """
+ self.code.append(" " * self.indent_amount)
+ self.code.append(line)
+ self.code.append("\n")
+
+ def indent(self):
+ """Increase the current indent for following lines."""
+ self.indent_amount += 4
+
+ def dedent(self):
+ """Decrease the current indent for following lines."""
+ self.indent_amount -= 4
+
+ def get_function(self, fn_name):
+ """Compile the code, and return the function `fn_name`."""
+ assert self.indent_amount == 0
+ g = {}
+ code_text = "".join(self.code)
+ exec(code_text, g)
+ return g[fn_name]
+
class Templite(object):
"""A simple template renderer, for a nano-subset of Django syntax.
@@ -39,22 +74,23 @@ class Templite(object):
for context in contexts:
self.context.update(context)
+ # We construct a function in source form, then compile it and hold onto
+ # it, and execute it to create the template output.
+ code = CodeBuilder()
+
+ code.add_line("def render(ctx, dot):")
+ code.indent()
+ code.add_line("result = []")
+ code.add_line("r = result.append")
+
# Split the text to form a list of tokens.
toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
- # Parse the tokens into a nested list of operations. Each item in the
- # list is a tuple with an opcode, and arguments. They'll be
- # interpreted by TempliteEngine.
- #
- # When parsing an action tag with nested content (if, for), the current
- # ops list is pushed onto ops_stack, and the parsing continues in a new
- # ops list that is part of the arguments to the if or for op.
- ops = []
ops_stack = []
for tok in toks:
if tok.startswith('{{'):
- # Expression: ('exp', expr)
- ops.append(('exp', tok[2:-2].strip()))
+ # An expression to evaluate.
+ code.add_line("r(str(%s))" % self.expr_code(tok[2:-2].strip()))
elif tok.startswith('{#'):
# Comment: ignore it and move on.
continue
@@ -62,30 +98,58 @@ class Templite(object):
# Action tag: split into words and parse further.
words = tok[2:-2].strip().split()
if words[0] == 'if':
- # If: ('if', (expr, body_ops))
- if_ops = []
+ # An if statement: evaluate the expression to determine if.
assert len(words) == 2
- ops.append(('if', (words[1], if_ops)))
- ops_stack.append(ops)
- ops = if_ops
+ ops_stack.append('if')
+ code.add_line("if %s:" % self.expr_code(words[1]))
+ code.indent()
elif words[0] == 'for':
- # For: ('for', (varname, listexpr, body_ops))
+ # A loop: iterate over expression result.
assert len(words) == 4 and words[2] == 'in'
- for_ops = []
- ops.append(('for', (words[1], words[3], for_ops)))
- ops_stack.append(ops)
- ops = for_ops
+ ops_stack.append('for')
+ code.add_line(
+ "for ctx[%r] in %s:" % (
+ words[1],
+ self.expr_code(words[3])
+ )
+ )
+ code.indent()
elif words[0].startswith('end'):
# Endsomething. Pop the ops stack
- ops = ops_stack.pop()
- assert ops[-1][0] == words[0][3:]
+ end_what = words[0][3:]
+ if ops_stack[-1] != end_what:
+ raise SyntaxError("Mismatched end tag: %r" % end_what)
+ ops_stack.pop()
+ code.dedent()
else:
- raise SyntaxError("Don't understand tag %r" % words)
+ raise SyntaxError("Don't understand tag: %r" % words[0])
else:
- ops.append(('lit', tok))
+ # Literal content. If it isn't empty, output it.
+ if tok:
+ code.add_line("r(%r)" % tok)
+
+ if ops_stack:
+ raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
- assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
- self.ops = ops
+ code.add_line("return ''.join(result)")
+ code.dedent()
+ self.render_function = code.get_function('render')
+
+ def expr_code(self, expr):
+ """Generate a Python expression for `expr`."""
+ if "|" in expr:
+ pipes = expr.split("|")
+ code = self.expr_code(pipes[0])
+ for func in pipes[1:]:
+ code = "ctx[%r](%s)" % (func, code)
+ elif "." in expr:
+ dots = expr.split(".")
+ code = self.expr_code(dots[0])
+ args = [repr(d) for d in dots[1:]]
+ code = "dot(%s, %s)" % (code, ", ".join(args))
+ else:
+ code = "ctx[%r]" % expr
+ return code
def render(self, context=None):
"""Render this template by applying it to `context`.
@@ -98,69 +162,15 @@ class Templite(object):
if context:
ctx.update(context)
- # Run it through an engine, and return the result.
- engine = _TempliteEngine(ctx)
- engine.execute(self.ops)
- return "".join(engine.result)
-
-
-class _TempliteEngine(object):
- """Executes Templite objects to produce strings."""
- def __init__(self, context):
- self.context = context
- self.result = []
-
- def execute(self, ops):
- """Execute `ops` in the engine.
-
- Called recursively for the bodies of if's and loops.
-
- """
- for op, args in ops:
- if op == 'lit':
- self.result.append(args)
- elif op == 'exp':
- try:
- self.result.append(str(self.evaluate(args)))
- except:
- exc_class, exc, _ = sys.exc_info()
- new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"
- % (args, exc))
- raise new_exc
- elif op == 'if':
- expr, body = args
- if self.evaluate(expr):
- self.execute(body)
- elif op == 'for':
- var, lis, body = args
- vals = self.evaluate(lis)
- for val in vals:
- self.context[var] = val
- self.execute(body)
- else:
- raise AssertionError("TempliteEngine doesn't grok op %r" % op)
-
- def evaluate(self, expr):
- """Evaluate an expression.
-
- `expr` can have pipes and dots to indicate data access and filtering.
-
- """
- if "|" in expr:
- pipes = expr.split("|")
- value = self.evaluate(pipes[0])
- for func in pipes[1:]:
- value = self.evaluate(func)(value)
- elif "." in expr:
- dots = expr.split('.')
- value = self.evaluate(dots[0])
- for dot in dots[1:]:
- try:
- value = getattr(value, dot)
- except AttributeError:
- value = value[dot]
- if hasattr(value, '__call__'):
- value = value()
- else:
- value = self.context[expr]
+ return self.render_function(ctx, self.do_dots)
+
+ def do_dots(self, value, *dots):
+ """Evaluate dotted expressions at runtime."""
+ for dot in dots:
+ try:
+ value = getattr(value, dot)
+ except AttributeError:
+ value = value[dot]
+ if hasattr(value, '__call__'):
+ value = value()
return value
diff --git a/tests/test_templite.py b/tests/test_templite.py
index 0435c545..a0bd182f 100644
--- a/tests/test_templite.py
+++ b/tests/test_templite.py
@@ -1,7 +1,7 @@
"""Tests for coverage.templite."""
from coverage.templite import Templite
-import unittest
+from tests.coveragetest import CoverageTest
# pylint: disable=W0612,E1101
# Disable W0612 (Unused variable) and
@@ -18,7 +18,7 @@ class AnyOldObject(object):
setattr(self, n, v)
-class TempliteTest(unittest.TestCase):
+class TempliteTest(CoverageTest):
"""Tests for Templite."""
def try_render(self, text, ctx, result):
@@ -165,6 +165,23 @@ class TempliteTest(unittest.TestCase):
"Hi, NEDBEN!"
)
+ def test_complex_if(self):
+ class Complex(AnyOldObject):
+ """A class to try out complex data access."""
+ def getit(self):
+ """Return it."""
+ return self.it
+ obj = Complex(it={'x':"Hello", 'y': 0})
+ self.try_render(
+ "@"
+ "{% if obj.getit.x %}X{% endif %}"
+ "{% if obj.getit.y %}Y{% endif %}"
+ "{% if obj.getit.y|str %}S{% endif %}"
+ "!",
+ { 'obj': obj, 'str': str },
+ "@XS!"
+ )
+
def test_loop_if(self):
self.try_render(
"@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!",
@@ -184,9 +201,11 @@ class TempliteTest(unittest.TestCase):
def test_nested_loops(self):
self.try_render(
- "@{% for n in nums %}"
+ "@"
+ "{% for n in nums %}"
"{% for a in abc %}{{a}}{{n}}{% endfor %}"
- "{% endfor %}!",
+ "{% endfor %}"
+ "!",
{'nums': [0,1,2], 'abc': ['a', 'b', 'c']},
"@a0b0c0a1b1c1a2b2c2!"
)
@@ -199,6 +218,20 @@ class TempliteTest(unittest.TestCase):
)
def test_bogus_tag_syntax(self):
- self.assertRaises(SyntaxError, self.try_render,
+ self.assertRaisesRegexp(
+ SyntaxError, "Don't understand tag: 'bogus'",
+ self.try_render,
"Huh: {% bogus %}!!{% endbogus %}??", {}, ""
)
+
+ def test_bad_nesting(self):
+ self.assertRaisesRegexp(
+ SyntaxError, "Unmatched action tag: 'if'",
+ self.try_render,
+ "{% if x %}X", {}, ""
+ )
+ self.assertRaisesRegexp(
+ SyntaxError, "Mismatched end tag: 'for'",
+ self.try_render,
+ "{% if x %}X{% endfor %}", {}, ""
+ )