--- 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