1. Ned Batchelder
  2. coverage.py

Commits

Ned Batchelder  committed 0a8e9a2

Templite now compiles to Python code for speed.

  • Participants
  • Parent commits e0c91cc
  • Branches default

Comments (0)

Files changed (4)

File CHANGES.txt

View file
 - 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

File TODO.txt

View file
     - 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

File coverage/templite.py

View file
 
 # 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.
         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
                 # 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)
 
-        assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
-        self.ops = ops
+        if ops_stack:
+            raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
+
+        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`.
         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)
+        return self.render_function(ctx, self.do_dots)
 
-
-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]
+    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

File tests/test_templite.py

View file
 """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
             setattr(self, n, v)
 
 
-class TempliteTest(unittest.TestCase):
+class TempliteTest(CoverageTest):
     """Tests for Templite."""
 
     def try_render(self, text, ctx, result):
             "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 %}!",
 
     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!"
             )
             )
 
     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 %}", {}, ""
+            )