eric7/DebugClients/Python/coverage/templite.py

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

eric ide

mercurial