DebugClients/Python/coverage/templite.py

changeset 3497
7f51ab29a1a2
parent 790
2c0ea0163ef4
child 3499
f2d4b02c7e88
equal deleted inserted replaced
3493:71f15675e89f 3497:7f51ab29a1a2
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 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ 3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
4 4
5 import re, sys 5 import re
6
7 from .backward import set # pylint: disable=W0622
8
9
10 class CodeBuilder(object):
11 """Build source code conveniently."""
12
13 def __init__(self, indent=0):
14 self.code = []
15 self.indent_amount = indent
16
17 def add_line(self, line):
18 """Add a line of source to the code.
19
20 Don't include indentations or newlines.
21
22 """
23 self.code.append(" " * self.indent_amount)
24 self.code.append(line)
25 self.code.append("\n")
26
27 def add_section(self):
28 """Add a section, a sub-CodeBuilder."""
29 sect = CodeBuilder(self.indent_amount)
30 self.code.append(sect)
31 return sect
32
33 def indent(self):
34 """Increase the current indent for following lines."""
35 self.indent_amount += 4
36
37 def dedent(self):
38 """Decrease the current indent for following lines."""
39 self.indent_amount -= 4
40
41 def __str__(self):
42 return "".join([str(c) for c in self.code])
43
44 def get_function(self, fn_name):
45 """Compile the code, and return the function `fn_name`."""
46 assert self.indent_amount == 0
47 g = {}
48 code_text = str(self)
49 exec(code_text, g)
50 return g[fn_name]
51
6 52
7 class Templite(object): 53 class Templite(object):
8 """A simple template renderer, for a nano-subset of Django syntax. 54 """A simple template renderer, for a nano-subset of Django syntax.
9 55
10 Supported constructs are extended variable access:: 56 Supported constructs are extended variable access::
37 self.text = text 83 self.text = text
38 self.context = {} 84 self.context = {}
39 for context in contexts: 85 for context in contexts:
40 self.context.update(context) 86 self.context.update(context)
41 87
88 # We construct a function in source form, then compile it and hold onto
89 # it, and execute it to render the template.
90 code = CodeBuilder()
91
92 code.add_line("def render(ctx, dot):")
93 code.indent()
94 vars_code = code.add_section()
95 self.all_vars = set()
96 self.loop_vars = set()
97 code.add_line("result = []")
98 code.add_line("a = result.append")
99 code.add_line("e = result.extend")
100 code.add_line("s = str")
101
102 buffered = []
103 def flush_output():
104 """Force `buffered` to the code builder."""
105 if len(buffered) == 1:
106 code.add_line("a(%s)" % buffered[0])
107 elif len(buffered) > 1:
108 code.add_line("e([%s])" % ",".join(buffered))
109 del buffered[:]
110
42 # Split the text to form a list of tokens. 111 # Split the text to form a list of tokens.
43 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) 112 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
44 113
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 = [] 114 ops_stack = []
54 for tok in toks: 115 for tok in toks:
55 if tok.startswith('{{'): 116 if tok.startswith('{{'):
56 # Expression: ('exp', expr) 117 # An expression to evaluate.
57 ops.append(('exp', tok[2:-2].strip())) 118 buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip()))
58 elif tok.startswith('{#'): 119 elif tok.startswith('{#'):
59 # Comment: ignore it and move on. 120 # Comment: ignore it and move on.
60 continue 121 continue
61 elif tok.startswith('{%'): 122 elif tok.startswith('{%'):
62 # Action tag: split into words and parse further. 123 # Action tag: split into words and parse further.
124 flush_output()
63 words = tok[2:-2].strip().split() 125 words = tok[2:-2].strip().split()
64 if words[0] == 'if': 126 if words[0] == 'if':
65 # If: ('if', (expr, body_ops)) 127 # An if statement: evaluate the expression to determine if.
66 if_ops = []
67 assert len(words) == 2 128 assert len(words) == 2
68 ops.append(('if', (words[1], if_ops))) 129 ops_stack.append('if')
69 ops_stack.append(ops) 130 code.add_line("if %s:" % self.expr_code(words[1]))
70 ops = if_ops 131 code.indent()
71 elif words[0] == 'for': 132 elif words[0] == 'for':
72 # For: ('for', (varname, listexpr, body_ops)) 133 # A loop: iterate over expression result.
73 assert len(words) == 4 and words[2] == 'in' 134 assert len(words) == 4 and words[2] == 'in'
74 for_ops = [] 135 ops_stack.append('for')
75 ops.append(('for', (words[1], words[3], for_ops))) 136 self.loop_vars.add(words[1])
76 ops_stack.append(ops) 137 code.add_line(
77 ops = for_ops 138 "for c_%s in %s:" % (
139 words[1],
140 self.expr_code(words[3])
141 )
142 )
143 code.indent()
78 elif words[0].startswith('end'): 144 elif words[0].startswith('end'):
79 # Endsomething. Pop the ops stack 145 # Endsomething. Pop the ops stack
80 ops = ops_stack.pop() 146 end_what = words[0][3:]
81 assert ops[-1][0] == words[0][3:] 147 if ops_stack[-1] != end_what:
148 raise SyntaxError("Mismatched end tag: %r" % end_what)
149 ops_stack.pop()
150 code.dedent()
82 else: 151 else:
83 raise SyntaxError("Don't understand tag %r" % words) 152 raise SyntaxError("Don't understand tag: %r" % words[0])
84 else: 153 else:
85 ops.append(('lit', tok)) 154 # Literal content. If it isn't empty, output it.
86 155 if tok:
87 assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0] 156 buffered.append("%r" % tok)
88 self.ops = ops 157 flush_output()
158
159 for var_name in self.all_vars - self.loop_vars:
160 vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name))
161
162 if ops_stack:
163 raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
164
165 code.add_line("return ''.join(result)")
166 code.dedent()
167 self.render_function = code.get_function('render')
168
169 def expr_code(self, expr):
170 """Generate a Python expression for `expr`."""
171 if "|" in expr:
172 pipes = expr.split("|")
173 code = self.expr_code(pipes[0])
174 for func in pipes[1:]:
175 self.all_vars.add(func)
176 code = "c_%s(%s)" % (func, code)
177 elif "." in expr:
178 dots = expr.split(".")
179 code = self.expr_code(dots[0])
180 args = [repr(d) for d in dots[1:]]
181 code = "dot(%s, %s)" % (code, ", ".join(args))
182 else:
183 self.all_vars.add(expr)
184 code = "c_%s" % expr
185 return code
89 186
90 def render(self, context=None): 187 def render(self, context=None):
91 """Render this template by applying it to `context`. 188 """Render this template by applying it to `context`.
92 189
93 `context` is a dictionary of values to use in this rendering. 190 `context` is a dictionary of values to use in this rendering.
95 """ 192 """
96 # Make the complete context we'll use. 193 # Make the complete context we'll use.
97 ctx = dict(self.context) 194 ctx = dict(self.context)
98 if context: 195 if context:
99 ctx.update(context) 196 ctx.update(context)
100 197 return self.render_function(ctx, self.do_dots)
101 # Run it through an engine, and return the result. 198
102 engine = _TempliteEngine(ctx) 199 def do_dots(self, value, *dots):
103 engine.execute(self.ops) 200 """Evaluate dotted expressions at runtime."""
104 return engine.result 201 for dot in dots:
105 202 try:
106 203 value = getattr(value, dot)
107 class _TempliteEngine(object): 204 except AttributeError:
108 """Executes Templite objects to produce strings.""" 205 value = value[dot]
109 def __init__(self, context): 206 if hasattr(value, '__call__'):
110 self.context = context 207 value = value()
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 += 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])
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 208 return value
167
168 #
169 # eflag: FileType = Python2

eric ide

mercurial