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 |
|