DebugClients/Python3/coverage/templite.py

changeset 29
391dc0bc4ae5
parent 0
de9c2efb9d02
child 3495
fac17a82b431
--- a/DebugClients/Python3/coverage/templite.py	Thu Jan 07 12:31:11 2010 +0000
+++ b/DebugClients/Python3/coverage/templite.py	Thu Jan 07 13:13:31 2010 +0000
@@ -1,105 +1,159 @@
 """A simple Python template renderer, for a nano-subset of Django syntax."""
 
-# Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html
-# and http://jtauber.com/2006/05/templates.html
-# and http://code.activestate.com/recipes/496730/
+# Coincidentally named the same as http://code.activestate.com/recipes/496702/
 
-import re
+import re, sys
 
 class Templite(object):
     """A simple template renderer, for a nano-subset of Django syntax.
 
     Supported constructs are extended variable access::
-    
+
         {{var.modifer.modifier|filter|filter}}
-        
-    and loops::
-    
+
+    loops::
+
         {% for var in list %}...{% endfor %}
-    
+
+    and ifs::
+
+        {% if var %}...{% endif %}
+
+    Comments are within curly-hash markers::
+
+        {# This will be ignored #}
+
     Construct a Templite with the template text, then use `render` against a
     dictionary context to create a finished string.
-    
+
     """
     def __init__(self, text, *contexts):
         """Construct a Templite with the given `text`.
-        
+
         `contexts` are dictionaries of values to use for future renderings.
         These are good for filters and global values.
-        
+
         """
-        self.loops = []
-        self.text = self._prepare(text)
+        self.text = text
         self.context = {}
         for context in contexts:
             self.context.update(context)
 
+        # 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()))
+            elif tok.startswith('{#'):
+                # Comment: ignore it and move on.
+                continue
+            elif tok.startswith('{%'):
+                # 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 = []
+                    assert len(words) == 2
+                    ops.append(('if', (words[1], if_ops)))
+                    ops_stack.append(ops)
+                    ops = if_ops
+                elif words[0] == 'for':
+                    # For: ('for', (varname, listexpr, body_ops))
+                    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
+                elif words[0].startswith('end'):
+                    # Endsomething.  Pop the ops stack
+                    ops = ops_stack.pop()
+                    assert ops[-1][0] == words[0][3:]
+                else:
+                    raise SyntaxError("Don't understand tag %r" % words)
+            else:
+                ops.append(('lit', tok))
+
+        assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
+        self.ops = ops
+
     def render(self, context=None):
         """Render this template by applying it to `context`.
-        
+
         `context` is a dictionary of values to use in this rendering.
-        
+
         """
         # Make the complete context we'll use.
         ctx = dict(self.context)
         if context:
             ctx.update(context)
-            
-        ctxaccess = _ContextAccess(ctx)
-        
-        # Render the loops.
-        for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops):
-            result = ""
-            for listval in ctxaccess[listvar]:
-                ctx[loopvar] = listval
-                result += loopbody % ctxaccess
-            ctx["loop:%d" % iloop] = result
-            
-        # Render the final template.
-        return self.text % ctxaccess
 
-    def _prepare(self, text):
-        """Convert Django-style data references into Python-native ones."""
-        # Pull out loops.
-        text = re.sub(
-            r"(?s){% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*?){% endfor %}",
-            self._loop_prepare, text
-            )
-        # Protect actual percent signs in the text.
-        text = text.replace("%", "%%")
-        # Convert {{foo}} into %(foo)s
-        text = re.sub(r"{{([^}]+)}}", r"%(\1)s", text)
-        return text
-
-    def _loop_prepare(self, match):
-        """Prepare a loop body for `_prepare`."""
-        nloop = len(self.loops)
-        # Append (loopvar, listvar, loopbody) to self.loops
-        loopvar, listvar, loopbody = match.groups()
-        loopbody = self._prepare(loopbody)
-        self.loops.append((loopvar, listvar, loopbody))
-        return "{{loop:%d}}" % nloop
+        # Run it through an engine, and return the result.
+        engine = _TempliteEngine(ctx)
+        engine.execute(self.ops)
+        return engine.result
 
 
-class _ContextAccess(object):
-    """A mediator for a context.
-    
-    Implements __getitem__ on a context for Templite, so that string formatting
-    references can pull data from the context.
-    
-    """
+class _TempliteEngine(object):
+    """Executes Templite objects to produce strings."""
     def __init__(self, context):
         self.context = context
+        self.result = ""
 
-    def __getitem__(self, key):
-        if "|" in key:
-            pipes = key.split("|")
-            value = self[pipes[0]]
+    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 += args
+            elif op == 'exp':
+                try:
+                    self.result += 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[func](value)
-        elif "." in key:
-            dots = key.split('.')
-            value = self[dots[0]]
+                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)
@@ -108,5 +162,5 @@
                 if hasattr(value, '__call__'):
                     value = value()
         else:
-            value = self.context[key]
+            value = self.context[expr]
         return value

eric ide

mercurial