DebugClients/Python/coverage/templite.py

changeset 4489
d0d6e4ad31bd
parent 3499
f2d4b02c7e88
child 4491
0d8612e24fef
equal deleted inserted replaced
4481:456c58fc64b0 4489:d0d6e4ad31bd
1 """A simple Python template renderer, for a nano-subset of Django syntax.""" 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4 """A simple Python template renderer, for a nano-subset of Django syntax.
5
6 For a detailed discussion of this code, see this chapter from 500 Lines:
7 http://aosabook.org/en/500L/a-template-engine.html
8
9 """
2 10
3 # Coincidentally named the same as http://code.activestate.com/recipes/496702/ 11 # Coincidentally named the same as http://code.activestate.com/recipes/496702/
4 12
5 import re 13 import re
6 14
7 from .backward import set # pylint: disable=W0622 15 from coverage import env
16
17
18 class TempliteSyntaxError(ValueError):
19 """Raised when a template has a syntax error."""
20 pass
21
22
23 class TempliteValueError(ValueError):
24 """Raised when an expression won't evaluate in a template."""
25 pass
8 26
9 27
10 class CodeBuilder(object): 28 class CodeBuilder(object):
11 """Build source code conveniently.""" 29 """Build source code conveniently."""
12 30
13 def __init__(self, indent=0): 31 def __init__(self, indent=0):
14 self.code = [] 32 self.code = []
15 self.indent_amount = indent 33 self.indent_level = indent
34
35 def __str__(self):
36 return "".join(str(c) for c in self.code)
16 37
17 def add_line(self, line): 38 def add_line(self, line):
18 """Add a line of source to the code. 39 """Add a line of source to the code.
19 40
20 Don't include indentations or newlines. 41 Indentation and newline will be added for you, don't provide them.
21 42
22 """ 43 """
23 self.code.append(" " * self.indent_amount) 44 self.code.extend([" " * self.indent_level, line, "\n"])
24 self.code.append(line)
25 self.code.append("\n")
26 45
27 def add_section(self): 46 def add_section(self):
28 """Add a section, a sub-CodeBuilder.""" 47 """Add a section, a sub-CodeBuilder."""
29 sect = CodeBuilder(self.indent_amount) 48 section = CodeBuilder(self.indent_level)
30 self.code.append(sect) 49 self.code.append(section)
31 return sect 50 return section
51
52 INDENT_STEP = 4 # PEP8 says so!
32 53
33 def indent(self): 54 def indent(self):
34 """Increase the current indent for following lines.""" 55 """Increase the current indent for following lines."""
35 self.indent_amount += 4 56 self.indent_level += self.INDENT_STEP
36 57
37 def dedent(self): 58 def dedent(self):
38 """Decrease the current indent for following lines.""" 59 """Decrease the current indent for following lines."""
39 self.indent_amount -= 4 60 self.indent_level -= self.INDENT_STEP
40 61
41 def __str__(self): 62 def get_globals(self):
42 return "".join([str(c) for c in self.code]) 63 """Execute the code, and return a dict of globals it defines."""
43 64 # A check that the caller really finished all the blocks they started.
44 def get_function(self, fn_name): 65 assert self.indent_level == 0
45 """Compile the code, and return the function `fn_name`.""" 66 # Get the Python source as a single string.
46 assert self.indent_amount == 0 67 python_source = str(self)
47 g = {} 68 # Execute the source, defining globals, and return them.
48 code_text = str(self) 69 global_namespace = {}
49 exec(code_text, g) 70 exec(python_source, global_namespace)
50 return g[fn_name] 71 return global_namespace
51 72
52 73
53 class Templite(object): 74 class Templite(object):
54 """A simple template renderer, for a nano-subset of Django syntax. 75 """A simple template renderer, for a nano-subset of Django syntax.
55 76
56 Supported constructs are extended variable access:: 77 Supported constructs are extended variable access::
57 78
58 {{var.modifer.modifier|filter|filter}} 79 {{var.modifier.modifier|filter|filter}}
59 80
60 loops:: 81 loops::
61 82
62 {% for var in list %}...{% endfor %} 83 {% for var in list %}...{% endfor %}
63 84
68 Comments are within curly-hash markers:: 89 Comments are within curly-hash markers::
69 90
70 {# This will be ignored #} 91 {# This will be ignored #}
71 92
72 Construct a Templite with the template text, then use `render` against a 93 Construct a Templite with the template text, then use `render` against a
73 dictionary context to create a finished string. 94 dictionary context to create a finished string::
95
96 templite = Templite('''
97 <h1>Hello {{name|upper}}!</h1>
98 {% for topic in topics %}
99 <p>You are interested in {{topic}}.</p>
100 {% endif %}
101 ''',
102 {'upper': str.upper},
103 )
104 text = templite.render({
105 'name': "Ned",
106 'topics': ['Python', 'Geometry', 'Juggling'],
107 })
74 108
75 """ 109 """
76 def __init__(self, text, *contexts): 110 def __init__(self, text, *contexts):
77 """Construct a Templite with the given `text`. 111 """Construct a Templite with the given `text`.
78 112
79 `contexts` are dictionaries of values to use for future renderings. 113 `contexts` are dictionaries of values to use for future renderings.
80 These are good for filters and global values. 114 These are good for filters and global values.
81 115
82 """ 116 """
83 self.text = text
84 self.context = {} 117 self.context = {}
85 for context in contexts: 118 for context in contexts:
86 self.context.update(context) 119 self.context.update(context)
87 120
121 self.all_vars = set()
122 self.loop_vars = set()
123
88 # We construct a function in source form, then compile it and hold onto 124 # We construct a function in source form, then compile it and hold onto
89 # it, and execute it to render the template. 125 # it, and execute it to render the template.
90 code = CodeBuilder() 126 code = CodeBuilder()
91 127
92 code.add_line("def render(ctx, dot):") 128 code.add_line("def render_function(context, do_dots):")
93 code.indent() 129 code.indent()
94 vars_code = code.add_section() 130 vars_code = code.add_section()
95 self.all_vars = set()
96 self.loop_vars = set()
97 code.add_line("result = []") 131 code.add_line("result = []")
98 code.add_line("a = result.append") 132 code.add_line("append_result = result.append")
99 code.add_line("e = result.extend") 133 code.add_line("extend_result = result.extend")
100 code.add_line("s = str") 134 if env.PY2:
135 code.add_line("to_str = unicode")
136 else:
137 code.add_line("to_str = str")
101 138
102 buffered = [] 139 buffered = []
140
103 def flush_output(): 141 def flush_output():
104 """Force `buffered` to the code builder.""" 142 """Force `buffered` to the code builder."""
105 if len(buffered) == 1: 143 if len(buffered) == 1:
106 code.add_line("a(%s)" % buffered[0]) 144 code.add_line("append_result(%s)" % buffered[0])
107 elif len(buffered) > 1: 145 elif len(buffered) > 1:
108 code.add_line("e([%s])" % ",".join(buffered)) 146 code.add_line("extend_result([%s])" % ", ".join(buffered))
109 del buffered[:] 147 del buffered[:]
110 148
149 ops_stack = []
150
111 # Split the text to form a list of tokens. 151 # Split the text to form a list of tokens.
112 toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) 152 tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
113 153
114 ops_stack = [] 154 for token in tokens:
115 for tok in toks: 155 if token.startswith('{#'):
116 if tok.startswith('{{'):
117 # An expression to evaluate.
118 buffered.append("s(%s)" % self.expr_code(tok[2:-2].strip()))
119 elif tok.startswith('{#'):
120 # Comment: ignore it and move on. 156 # Comment: ignore it and move on.
121 continue 157 continue
122 elif tok.startswith('{%'): 158 elif token.startswith('{{'):
159 # An expression to evaluate.
160 expr = self._expr_code(token[2:-2].strip())
161 buffered.append("to_str(%s)" % expr)
162 elif token.startswith('{%'):
123 # Action tag: split into words and parse further. 163 # Action tag: split into words and parse further.
124 flush_output() 164 flush_output()
125 words = tok[2:-2].strip().split() 165 words = token[2:-2].strip().split()
126 if words[0] == 'if': 166 if words[0] == 'if':
127 # An if statement: evaluate the expression to determine if. 167 # An if statement: evaluate the expression to determine if.
128 assert len(words) == 2 168 if len(words) != 2:
169 self._syntax_error("Don't understand if", token)
129 ops_stack.append('if') 170 ops_stack.append('if')
130 code.add_line("if %s:" % self.expr_code(words[1])) 171 code.add_line("if %s:" % self._expr_code(words[1]))
131 code.indent() 172 code.indent()
132 elif words[0] == 'for': 173 elif words[0] == 'for':
133 # A loop: iterate over expression result. 174 # A loop: iterate over expression result.
134 assert len(words) == 4 and words[2] == 'in' 175 if len(words) != 4 or words[2] != 'in':
176 self._syntax_error("Don't understand for", token)
135 ops_stack.append('for') 177 ops_stack.append('for')
136 self.loop_vars.add(words[1]) 178 self._variable(words[1], self.loop_vars)
137 code.add_line( 179 code.add_line(
138 "for c_%s in %s:" % ( 180 "for c_%s in %s:" % (
139 words[1], 181 words[1],
140 self.expr_code(words[3]) 182 self._expr_code(words[3])
141 ) 183 )
142 ) 184 )
143 code.indent() 185 code.indent()
144 elif words[0].startswith('end'): 186 elif words[0].startswith('end'):
145 # Endsomething. Pop the ops stack 187 # Endsomething. Pop the ops stack.
188 if len(words) != 1:
189 self._syntax_error("Don't understand end", token)
146 end_what = words[0][3:] 190 end_what = words[0][3:]
147 if ops_stack[-1] != end_what: 191 if not ops_stack:
148 raise SyntaxError("Mismatched end tag: %r" % end_what) 192 self._syntax_error("Too many ends", token)
149 ops_stack.pop() 193 start_what = ops_stack.pop()
194 if start_what != end_what:
195 self._syntax_error("Mismatched end tag", end_what)
150 code.dedent() 196 code.dedent()
151 else: 197 else:
152 raise SyntaxError("Don't understand tag: %r" % words[0]) 198 self._syntax_error("Don't understand tag", words[0])
153 else: 199 else:
154 # Literal content. If it isn't empty, output it. 200 # Literal content. If it isn't empty, output it.
155 if tok: 201 if token:
156 buffered.append("%r" % tok) 202 buffered.append(repr(token))
203
204 if ops_stack:
205 self._syntax_error("Unmatched action tag", ops_stack[-1])
206
157 flush_output() 207 flush_output()
158 208
159 for var_name in self.all_vars - self.loop_vars: 209 for var_name in self.all_vars - self.loop_vars:
160 vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name)) 210 vars_code.add_line("c_%s = context[%r]" % (var_name, var_name))
161 211
162 if ops_stack: 212 code.add_line('return "".join(result)')
163 raise SyntaxError("Unmatched action tag: %r" % ops_stack[-1])
164
165 code.add_line("return ''.join(result)")
166 code.dedent() 213 code.dedent()
167 self.render_function = code.get_function('render') 214 self._render_function = code.get_globals()['render_function']
168 215
169 def expr_code(self, expr): 216 def _expr_code(self, expr):
170 """Generate a Python expression for `expr`.""" 217 """Generate a Python expression for `expr`."""
171 if "|" in expr: 218 if "|" in expr:
172 pipes = expr.split("|") 219 pipes = expr.split("|")
173 code = self.expr_code(pipes[0]) 220 code = self._expr_code(pipes[0])
174 for func in pipes[1:]: 221 for func in pipes[1:]:
175 self.all_vars.add(func) 222 self._variable(func, self.all_vars)
176 code = "c_%s(%s)" % (func, code) 223 code = "c_%s(%s)" % (func, code)
177 elif "." in expr: 224 elif "." in expr:
178 dots = expr.split(".") 225 dots = expr.split(".")
179 code = self.expr_code(dots[0]) 226 code = self._expr_code(dots[0])
180 args = [repr(d) for d in dots[1:]] 227 args = ", ".join(repr(d) for d in dots[1:])
181 code = "dot(%s, %s)" % (code, ", ".join(args)) 228 code = "do_dots(%s, %s)" % (code, args)
182 else: 229 else:
183 self.all_vars.add(expr) 230 self._variable(expr, self.all_vars)
184 code = "c_%s" % expr 231 code = "c_%s" % expr
185 return code 232 return code
186 233
234 def _syntax_error(self, msg, thing):
235 """Raise a syntax error using `msg`, and showing `thing`."""
236 raise TempliteSyntaxError("%s: %r" % (msg, thing))
237
238 def _variable(self, name, vars_set):
239 """Track that `name` is used as a variable.
240
241 Adds the name to `vars_set`, a set of variable names.
242
243 Raises an syntax error if `name` is not a valid name.
244
245 """
246 if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name):
247 self._syntax_error("Not a valid name", name)
248 vars_set.add(name)
249
187 def render(self, context=None): 250 def render(self, context=None):
188 """Render this template by applying it to `context`. 251 """Render this template by applying it to `context`.
189 252
190 `context` is a dictionary of values to use in this rendering. 253 `context` is a dictionary of values to use in this rendering.
191 254
192 """ 255 """
193 # Make the complete context we'll use. 256 # Make the complete context we'll use.
194 ctx = dict(self.context) 257 render_context = dict(self.context)
195 if context: 258 if context:
196 ctx.update(context) 259 render_context.update(context)
197 return self.render_function(ctx, self.do_dots) 260 return self._render_function(render_context, self._do_dots)
198 261
199 def do_dots(self, value, *dots): 262 def _do_dots(self, value, *dots):
200 """Evaluate dotted expressions at runtime.""" 263 """Evaluate dotted expressions at run-time."""
201 for dot in dots: 264 for dot in dots:
202 try: 265 try:
203 value = getattr(value, dot) 266 value = getattr(value, dot)
204 except AttributeError: 267 except AttributeError:
205 value = value[dot] 268 try:
206 if hasattr(value, '__call__'): 269 value = value[dot]
270 except (TypeError, KeyError):
271 raise TempliteValueError(
272 "Couldn't evaluate %r.%s" % (value, dot)
273 )
274 if callable(value):
207 value = value() 275 value = value()
208 return value 276 return value
209
210 #
211 # eflag: FileType = Python2

eric ide

mercurial