DebugClients/Python/coverage/templite.py

changeset 4491
0d8612e24fef
parent 4489
d0d6e4ad31bd
child 5051
3586ebd9fac8
--- a/DebugClients/Python/coverage/templite.py	Sat Oct 10 12:06:10 2015 +0200
+++ b/DebugClients/Python/coverage/templite.py	Sat Oct 10 12:44:52 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,19 +254,24 @@
 
         """
         # 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
 

eric ide

mercurial