Home | History | Annotate | Download | only in coverage
      1 """A simple Python template renderer, for a nano-subset of Django syntax."""
      2 
      3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
      4 
      5 import re, sys
      6 
      7 class Templite(object):
      8     """A simple template renderer, for a nano-subset of Django syntax.
      9 
     10     Supported constructs are extended variable access::
     11 
     12         {{var.modifer.modifier|filter|filter}}
     13 
     14     loops::
     15 
     16         {% for var in list %}...{% endfor %}
     17 
     18     and ifs::
     19 
     20         {% if var %}...{% endif %}
     21 
     22     Comments are within curly-hash markers::
     23 
     24         {# This will be ignored #}
     25 
     26     Construct a Templite with the template text, then use `render` against a
     27     dictionary context to create a finished string.
     28 
     29     """
     30     def __init__(self, text, *contexts):
     31         """Construct a Templite with the given `text`.
     32 
     33         `contexts` are dictionaries of values to use for future renderings.
     34         These are good for filters and global values.
     35 
     36         """
     37         self.text = text
     38         self.context = {}
     39         for context in contexts:
     40             self.context.update(context)
     41 
     42         # Split the text to form a list of tokens.
     43         toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
     44 
     45         # Parse the tokens into a nested list of operations.  Each item in the
     46         # list is a tuple with an opcode, and arguments.  They'll be
     47         # interpreted by TempliteEngine.
     48         #
     49         # When parsing an action tag with nested content (if, for), the current
     50         # ops list is pushed onto ops_stack, and the parsing continues in a new
     51         # ops list that is part of the arguments to the if or for op.
     52         ops = []
     53         ops_stack = []
     54         for tok in toks:
     55             if tok.startswith('{{'):
     56                 # Expression: ('exp', expr)
     57                 ops.append(('exp', tok[2:-2].strip()))
     58             elif tok.startswith('{#'):
     59                 # Comment: ignore it and move on.
     60                 continue
     61             elif tok.startswith('{%'):
     62                 # Action tag: split into words and parse further.
     63                 words = tok[2:-2].strip().split()
     64                 if words[0] == 'if':
     65                     # If: ('if', (expr, body_ops))
     66                     if_ops = []
     67                     assert len(words) == 2
     68                     ops.append(('if', (words[1], if_ops)))
     69                     ops_stack.append(ops)
     70                     ops = if_ops
     71                 elif words[0] == 'for':
     72                     # For: ('for', (varname, listexpr, body_ops))
     73                     assert len(words) == 4 and words[2] == 'in'
     74                     for_ops = []
     75                     ops.append(('for', (words[1], words[3], for_ops)))
     76                     ops_stack.append(ops)
     77                     ops = for_ops
     78                 elif words[0].startswith('end'):
     79                     # Endsomething.  Pop the ops stack
     80                     ops = ops_stack.pop()
     81                     assert ops[-1][0] == words[0][3:]
     82                 else:
     83                     raise SyntaxError("Don't understand tag %r" % words)
     84             else:
     85                 ops.append(('lit', tok))
     86 
     87         assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
     88         self.ops = ops
     89 
     90     def render(self, context=None):
     91         """Render this template by applying it to `context`.
     92 
     93         `context` is a dictionary of values to use in this rendering.
     94 
     95         """
     96         # Make the complete context we'll use.
     97         ctx = dict(self.context)
     98         if context:
     99             ctx.update(context)
    100 
    101         # Run it through an engine, and return the result.
    102         engine = _TempliteEngine(ctx)
    103         engine.execute(self.ops)
    104         return "".join(engine.result)
    105 
    106 
    107 class _TempliteEngine(object):
    108     """Executes Templite objects to produce strings."""
    109     def __init__(self, context):
    110         self.context = context
    111         self.result = []
    112 
    113     def execute(self, ops):
    114         """Execute `ops` in the engine.
    115 
    116         Called recursively for the bodies of if's and loops.
    117 
    118         """
    119         for op, args in ops:
    120             if op == 'lit':
    121                 self.result.append(args)
    122             elif op == 'exp':
    123                 try:
    124                     self.result.append(str(self.evaluate(args)))
    125                 except:
    126                     exc_class, exc, _ = sys.exc_info()
    127                     new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"
    128                                         % (args, exc))
    129                     raise new_exc
    130             elif op == 'if':
    131                 expr, body = args
    132                 if self.evaluate(expr):
    133                     self.execute(body)
    134             elif op == 'for':
    135                 var, lis, body = args
    136                 vals = self.evaluate(lis)
    137                 for val in vals:
    138                     self.context[var] = val
    139                     self.execute(body)
    140             else:
    141                 raise AssertionError("TempliteEngine doesn't grok op %r" % op)
    142 
    143     def evaluate(self, expr):
    144         """Evaluate an expression.
    145 
    146         `expr` can have pipes and dots to indicate data access and filtering.
    147 
    148         """
    149         if "|" in expr:
    150             pipes = expr.split("|")
    151             value = self.evaluate(pipes[0])
    152             for func in pipes[1:]:
    153                 value = self.evaluate(func)(value)
    154         elif "." in expr:
    155             dots = expr.split('.')
    156             value = self.evaluate(dots[0])
    157             for dot in dots[1:]:
    158                 try:
    159                     value = getattr(value, dot)
    160                 except AttributeError:
    161                     value = value[dot]
    162                 if hasattr(value, '__call__'):
    163                     value = value()
    164         else:
    165             value = self.context[expr]
    166         return value
    167