src/eric7/DebugClients/Python/coverage/templite.py

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

eric ide

mercurial