DebugClients/Python/coverage/templite.py

changeset 31
744cd0b4b8cd
parent 0
de9c2efb9d02
child 790
2c0ea0163ef4
equal deleted inserted replaced
30:9513afbd57f1 31:744cd0b4b8cd
1 """A simple Python template renderer, for a nano-subset of Django syntax.""" 1 """A simple Python template renderer, for a nano-subset of Django syntax."""
2 2
3 # Started from http://blog.ianbicking.org/templating-via-dict-wrappers.html 3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
4 # and http://jtauber.com/2006/05/templates.html
5 # and http://code.activestate.com/recipes/496730/
6 4
7 import re 5 import re, sys
8 6
9 class Templite(object): 7 class Templite(object):
10 """A simple template renderer, for a nano-subset of Django syntax. 8 """A simple template renderer, for a nano-subset of Django syntax.
11 9
12 Supported constructs are extended variable access:: 10 Supported constructs are extended variable access::
13 11
14 {{var.modifer.modifier|filter|filter}} 12 {{var.modifer.modifier|filter|filter}}
15 13
16 and loops:: 14 loops::
17 15
18 {% for var in list %}...{% endfor %} 16 {% for var in list %}...{% endfor %}
19 17
18 and ifs::
19
20 {% if var %}...{% endif %}
21
22 Comments are within curly-hash markers::
23
24 {# This will be ignored #}
25
20 Construct a Templite with the template text, then use `render` against a 26 Construct a Templite with the template text, then use `render` against a
21 dictionary context to create a finished string. 27 dictionary context to create a finished string.
22 28
23 """ 29 """
24 def __init__(self, text, *contexts): 30 def __init__(self, text, *contexts):
25 """Construct a Templite with the given `text`. 31 """Construct a Templite with the given `text`.
26 32
27 `contexts` are dictionaries of values to use for future renderings. 33 `contexts` are dictionaries of values to use for future renderings.
28 These are good for filters and global values. 34 These are good for filters and global values.
29 35
30 """ 36 """
31 self.loops = [] 37 self.text = text
32 self.text = self._prepare(text)
33 self.context = {} 38 self.context = {}
34 for context in contexts: 39 for context in contexts:
35 self.context.update(context) 40 self.context.update(context)
36 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
37 def render(self, context=None): 90 def render(self, context=None):
38 """Render this template by applying it to `context`. 91 """Render this template by applying it to `context`.
39 92
40 `context` is a dictionary of values to use in this rendering. 93 `context` is a dictionary of values to use in this rendering.
41 94
42 """ 95 """
43 # Make the complete context we'll use. 96 # Make the complete context we'll use.
44 ctx = dict(self.context) 97 ctx = dict(self.context)
45 if context: 98 if context:
46 ctx.update(context) 99 ctx.update(context)
47
48 ctxaccess = _ContextAccess(ctx)
49
50 # Render the loops.
51 for iloop, (loopvar, listvar, loopbody) in enumerate(self.loops):
52 result = ""
53 for listval in ctxaccess[listvar]:
54 ctx[loopvar] = listval
55 result += loopbody % ctxaccess
56 ctx["loop:%d" % iloop] = result
57
58 # Render the final template.
59 return self.text % ctxaccess
60 100
61 def _prepare(self, text): 101 # Run it through an engine, and return the result.
62 """Convert Django-style data references into Python-native ones.""" 102 engine = _TempliteEngine(ctx)
63 # Pull out loops. 103 engine.execute(self.ops)
64 text = re.sub( 104 return engine.result
65 r"(?s){% for ([a-z0-9_]+) in ([a-z0-9_.|]+) %}(.*?){% endfor %}",
66 self._loop_prepare, text
67 )
68 # Protect actual percent signs in the text.
69 text = text.replace("%", "%%")
70 # Convert {{foo}} into %(foo)s
71 text = re.sub(r"{{([^}]+)}}", r"%(\1)s", text)
72 return text
73
74 def _loop_prepare(self, match):
75 """Prepare a loop body for `_prepare`."""
76 nloop = len(self.loops)
77 # Append (loopvar, listvar, loopbody) to self.loops
78 loopvar, listvar, loopbody = match.groups()
79 loopbody = self._prepare(loopbody)
80 self.loops.append((loopvar, listvar, loopbody))
81 return "{{loop:%d}}" % nloop
82 105
83 106
84 class _ContextAccess(object): 107 class _TempliteEngine(object):
85 """A mediator for a context. 108 """Executes Templite objects to produce strings."""
86
87 Implements __getitem__ on a context for Templite, so that string formatting
88 references can pull data from the context.
89
90 """
91 def __init__(self, context): 109 def __init__(self, context):
92 self.context = context 110 self.context = context
111 self.result = ""
93 112
94 def __getitem__(self, key): 113 def execute(self, ops):
95 if "|" in key: 114 """Execute `ops` in the engine.
96 pipes = key.split("|") 115
97 value = self[pipes[0]] 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 += args
122 elif op == 'exp':
123 try:
124 self.result += 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])
98 for func in pipes[1:]: 152 for func in pipes[1:]:
99 value = self[func](value) 153 value = self.evaluate(func)(value)
100 elif "." in key: 154 elif "." in expr:
101 dots = key.split('.') 155 dots = expr.split('.')
102 value = self[dots[0]] 156 value = self.evaluate(dots[0])
103 for dot in dots[1:]: 157 for dot in dots[1:]:
104 try: 158 try:
105 value = getattr(value, dot) 159 value = getattr(value, dot)
106 except AttributeError: 160 except AttributeError:
107 value = value[dot] 161 value = value[dot]
108 if callable(value): 162 if hasattr(value, '__call__'):
109 value = value() 163 value = value()
110 else: 164 else:
111 value = self.context[key] 165 value = self.context[expr]
112 return value 166 return value

eric ide

mercurial