From 62f4ed9cd969267f4d09a03a07b89144cd511faf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 26 Oct 2013 13:18:41 -0400 Subject: Templite now compiles to Python code for speed. --- coverage/templite.py | 194 +++++++++++++++++++++++++++------------------------ 1 file changed, 102 insertions(+), 92 deletions(-) (limited to 'coverage/templite.py') diff --git a/coverage/templite.py b/coverage/templite.py index c39e061..2ca12fe 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 -- cgit v1.2.1 From fc10540c2c0a845502912a207e76fbf600dca552 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 27 Oct 2013 21:11:34 -0400 Subject: More templite fiddling: use locals instead of ctx[] each time. --- coverage/templite.py | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) (limited to 'coverage/templite.py') diff --git a/coverage/templite.py b/coverage/templite.py index 2ca12fe..088d74f 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -8,9 +8,9 @@ import re class CodeBuilder(object): """Build source code conveniently.""" - def __init__(self): + def __init__(self, indent=0): self.code = [] - self.indent_amount = 0 + self.indent_amount = indent def add_line(self, line): """Add a line of source to the code. @@ -22,6 +22,12 @@ class CodeBuilder(object): self.code.append(line) self.code.append("\n") + def add_section(self): + """Add a section, a sub-CodeBuilder.""" + sect = CodeBuilder(self.indent_amount) + self.code.append(sect) + return sect + def indent(self): """Increase the current indent for following lines.""" self.indent_amount += 4 @@ -30,11 +36,14 @@ class CodeBuilder(object): """Decrease the current indent for following lines.""" self.indent_amount -= 4 + def __str__(self): + return "".join(str(c) for c in self.code) + 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) + code_text = str(self) exec(code_text, g) return g[fn_name] @@ -75,13 +84,27 @@ class Templite(object): 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. + # it, and execute it to render the template. code = CodeBuilder() code.add_line("def render(ctx, dot):") code.indent() + vars_code = code.add_section() + self.all_vars = set() + self.loop_vars = set() code.add_line("result = []") - code.add_line("r = result.append") + code.add_line("a = result.append") + code.add_line("e = result.extend") + code.add_line("s = str") + + buffered = [] + def flush_output(): + """Force `buffered` to the code builder.""" + if len(buffered) == 1: + code.add_line("a(%s)" % buffered[0]) + elif len(buffered) > 1: + code.add_line("e([%s])" % ",".join(buffered)) + del buffered[:] # Split the text to form a list of tokens. toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) @@ -90,12 +113,13 @@ class Templite(object): for tok in toks: if tok.startswith('{{'): # An expression to evaluate. - code.add_line("r(str(%s))" % self.expr_code(tok[2:-2].strip())) + buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip())) elif tok.startswith('{#'): # Comment: ignore it and move on. continue elif tok.startswith('{%'): # Action tag: split into words and parse further. + flush_output() words = tok[2:-2].strip().split() if words[0] == 'if': # An if statement: evaluate the expression to determine if. @@ -107,8 +131,9 @@ class Templite(object): # A loop: iterate over expression result. assert len(words) == 4 and words[2] == 'in' ops_stack.append('for') + self.loop_vars.add(words[1]) code.add_line( - "for ctx[%r] in %s:" % ( + "for c_%s in %s:" % ( words[1], self.expr_code(words[3]) ) @@ -126,7 +151,11 @@ class Templite(object): else: # Literal content. If it isn't empty, output it. if tok: - code.add_line("r(%r)" % tok) + buffered.append("%r" % tok) + flush_output() + + for var_name in self.all_vars - self.loop_vars: + vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name)) if ops_stack: raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1]) @@ -141,14 +170,16 @@ class Templite(object): pipes = expr.split("|") code = self.expr_code(pipes[0]) for func in pipes[1:]: - code = "ctx[%r](%s)" % (func, code) + self.all_vars.add(func) + code = "c_%s(%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 + self.all_vars.add(expr) + code = "c_%s" % expr return code def render(self, context=None): @@ -161,7 +192,6 @@ class Templite(object): ctx = dict(self.context) if context: ctx.update(context) - return self.render_function(ctx, self.do_dots) def do_dots(self, value, *dots): -- cgit v1.2.1 From 076c347d01ff193b477b67992ffc6e763d431efd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 27 Oct 2013 21:23:38 -0400 Subject: Python 2.3, my old nemesis! --- coverage/templite.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'coverage/templite.py') diff --git a/coverage/templite.py b/coverage/templite.py index 088d74f..e5c0baf 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -4,6 +4,8 @@ import re +from coverage.backward import set # pylint: disable=W0622 + class CodeBuilder(object): """Build source code conveniently.""" @@ -37,7 +39,7 @@ class CodeBuilder(object): self.indent_amount -= 4 def __str__(self): - return "".join(str(c) for c in self.code) + return "".join([str(c) for c in self.code]) def get_function(self, fn_name): """Compile the code, and return the function `fn_name`.""" -- cgit v1.2.1