diff -r 456c58fc64b0 -r d0d6e4ad31bd DebugClients/Python/coverage/templite.py --- a/DebugClients/Python/coverage/templite.py Sun Oct 04 13:35:09 2015 +0200 +++ b/DebugClients/Python/coverage/templite.py Sun Oct 04 22:37:56 2015 +0200 @@ -1,10 +1,28 @@ -"""A simple Python template renderer, for a nano-subset of Django syntax.""" +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""A simple Python template renderer, for a nano-subset of Django syntax. + +For a detailed discussion of this code, see this chapter from 500 Lines: +http://aosabook.org/en/500L/a-template-engine.html + +""" # Coincidentally named the same as http://code.activestate.com/recipes/496702/ import re -from .backward import set # pylint: disable=W0622 +from coverage import env + + +class TempliteSyntaxError(ValueError): + """Raised when a template has a syntax error.""" + pass + + +class TempliteValueError(ValueError): + """Raised when an expression won't evaluate in a template.""" + pass class CodeBuilder(object): @@ -12,42 +30,45 @@ def __init__(self, indent=0): self.code = [] - self.indent_amount = indent + self.indent_level = indent + + def __str__(self): + return "".join(str(c) for c in self.code) def add_line(self, line): """Add a line of source to the code. - Don't include indentations or newlines. + Indentation and newline will be added for you, don't provide them. """ - self.code.append(" " * self.indent_amount) - self.code.append(line) - self.code.append("\n") + self.code.extend([" " * self.indent_level, line, "\n"]) def add_section(self): """Add a section, a sub-CodeBuilder.""" - sect = CodeBuilder(self.indent_amount) - self.code.append(sect) - return sect + section = CodeBuilder(self.indent_level) + self.code.append(section) + return section + + INDENT_STEP = 4 # PEP8 says so! def indent(self): """Increase the current indent for following lines.""" - self.indent_amount += 4 + self.indent_level += self.INDENT_STEP def dedent(self): """Decrease the current indent for following lines.""" - self.indent_amount -= 4 - - def __str__(self): - return "".join([str(c) for c in self.code]) + self.indent_level -= self.INDENT_STEP - def get_function(self, fn_name): - """Compile the code, and return the function `fn_name`.""" - assert self.indent_amount == 0 - g = {} - code_text = str(self) - exec(code_text, g) - return g[fn_name] + def get_globals(self): + """Execute the code, and return a dict of globals it defines.""" + # A check that the caller really finished all the blocks they started. + assert self.indent_level == 0 + # Get the Python source as a single string. + python_source = str(self) + # Execute the source, defining globals, and return them. + global_namespace = {} + exec(python_source, global_namespace) + return global_namespace class Templite(object): @@ -55,7 +76,7 @@ Supported constructs are extended variable access:: - {{var.modifer.modifier|filter|filter}} + {{var.modifier.modifier|filter|filter}} loops:: @@ -70,7 +91,20 @@ {# This will be ignored #} Construct a Templite with the template text, then use `render` against a - dictionary context to create a finished string. + dictionary context to create a finished string:: + + templite = Templite(''' + <h1>Hello {{name|upper}}!</h1> + {% for topic in topics %} + <p>You are interested in {{topic}}.</p> + {% endif %} + ''', + {'upper': str.upper}, + ) + text = templite.render({ + 'name': "Ned", + 'topics': ['Python', 'Geometry', 'Juggling'], + }) """ def __init__(self, text, *contexts): @@ -80,110 +114,139 @@ These are good for filters and global values. """ - self.text = text self.context = {} for context in contexts: self.context.update(context) + self.all_vars = set() + self.loop_vars = set() + # We construct a function in source form, then compile it and hold onto # it, and execute it to render the template. code = CodeBuilder() - code.add_line("def render(ctx, dot):") + code.add_line("def render_function(context, do_dots):") code.indent() vars_code = code.add_section() - self.all_vars = set() - self.loop_vars = set() code.add_line("result = []") - code.add_line("a = result.append") - code.add_line("e = result.extend") - code.add_line("s = str") + code.add_line("append_result = result.append") + code.add_line("extend_result = result.extend") + if env.PY2: + code.add_line("to_str = unicode") + else: + code.add_line("to_str = str") buffered = [] + def flush_output(): """Force `buffered` to the code builder.""" if len(buffered) == 1: - code.add_line("a(%s)" % buffered[0]) + code.add_line("append_result(%s)" % buffered[0]) elif len(buffered) > 1: - code.add_line("e([%s])" % ",".join(buffered)) + code.add_line("extend_result([%s])" % ", ".join(buffered)) del buffered[:] - # Split the text to form a list of tokens. - toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) - ops_stack = [] - for tok in toks: - if tok.startswith('{{'): - # An expression to evaluate. - buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip())) - elif tok.startswith('{#'): + + # Split the text to form a list of tokens. + tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) + + for token in tokens: + if token.startswith('{#'): # Comment: ignore it and move on. continue - elif tok.startswith('{%'): + elif token.startswith('{{'): + # An expression to evaluate. + expr = self._expr_code(token[2:-2].strip()) + buffered.append("to_str(%s)" % expr) + elif token.startswith('{%'): # Action tag: split into words and parse further. flush_output() - words = tok[2:-2].strip().split() + words = token[2:-2].strip().split() if words[0] == 'if': # An if statement: evaluate the expression to determine if. - assert len(words) == 2 + if len(words) != 2: + self._syntax_error("Don't understand if", token) ops_stack.append('if') - code.add_line("if %s:" % self.expr_code(words[1])) + code.add_line("if %s:" % self._expr_code(words[1])) code.indent() elif words[0] == 'for': # A loop: iterate over expression result. - assert len(words) == 4 and words[2] == 'in' + if len(words) != 4 or words[2] != 'in': + self._syntax_error("Don't understand for", token) ops_stack.append('for') - self.loop_vars.add(words[1]) + self._variable(words[1], self.loop_vars) code.add_line( "for c_%s in %s:" % ( words[1], - self.expr_code(words[3]) + self._expr_code(words[3]) ) ) code.indent() elif words[0].startswith('end'): - # Endsomething. Pop the ops stack + # Endsomething. Pop the ops stack. + if len(words) != 1: + self._syntax_error("Don't understand end", token) end_what = words[0][3:] - if ops_stack[-1] != end_what: - raise SyntaxError("Mismatched end tag: %r" % end_what) - ops_stack.pop() + if not ops_stack: + self._syntax_error("Too many ends", token) + start_what = ops_stack.pop() + if start_what != end_what: + self._syntax_error("Mismatched end tag", end_what) code.dedent() else: - raise SyntaxError("Don't understand tag: %r" % words[0]) + self._syntax_error("Don't understand tag", words[0]) else: # Literal content. If it isn't empty, output it. - if tok: - buffered.append("%r" % tok) + if token: + buffered.append(repr(token)) + + if ops_stack: + self._syntax_error("Unmatched action tag", ops_stack[-1]) + 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]) + vars_code.add_line("c_%s = context[%r]" % (var_name, var_name)) - code.add_line("return ''.join(result)") + code.add_line('return "".join(result)') code.dedent() - self.render_function = code.get_function('render') + self._render_function = code.get_globals()['render_function'] - def expr_code(self, expr): + def _expr_code(self, expr): """Generate a Python expression for `expr`.""" if "|" in expr: pipes = expr.split("|") - code = self.expr_code(pipes[0]) + code = self._expr_code(pipes[0]) for func in pipes[1:]: - self.all_vars.add(func) + self._variable(func, self.all_vars) 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)) + code = self._expr_code(dots[0]) + args = ", ".join(repr(d) for d in dots[1:]) + code = "do_dots(%s, %s)" % (code, args) else: - self.all_vars.add(expr) + self._variable(expr, self.all_vars) code = "c_%s" % expr return code + def _syntax_error(self, msg, thing): + """Raise a syntax error using `msg`, and showing `thing`.""" + raise TempliteSyntaxError("%s: %r" % (msg, thing)) + + def _variable(self, name, vars_set): + """Track that `name` is used as a variable. + + Adds the name to `vars_set`, a set of variable names. + + Raises an syntax error if `name` is not a valid name. + + """ + if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name): + self._syntax_error("Not a valid name", name) + vars_set.add(name) + def render(self, context=None): """Render this template by applying it to `context`. @@ -191,21 +254,23 @@ """ # Make the complete context we'll use. - ctx = dict(self.context) + render_context = dict(self.context) if context: - ctx.update(context) - return self.render_function(ctx, self.do_dots) + render_context.update(context) + return self._render_function(render_context, self._do_dots) - def do_dots(self, value, *dots): - """Evaluate dotted expressions at runtime.""" + def _do_dots(self, value, *dots): + """Evaluate dotted expressions at run-time.""" for dot in dots: try: value = getattr(value, dot) except AttributeError: - value = value[dot] - if hasattr(value, '__call__'): + try: + value = value[dot] + except (TypeError, KeyError): + raise TempliteValueError( + "Couldn't evaluate %r.%s" % (value, dot) + ) + if callable(value): value = value() return value - -# -# eflag: FileType = Python2