Coverage for coverage / templite.py: 100.000%
157 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""A simple Python template renderer, for a nano-subset of Django syntax.
6For a detailed discussion of this code, see this chapter from 500 Lines:
7http://aosabook.org/en/500L/a-template-engine.html
9"""
11# Coincidentally named the same as http://code.activestate.com/recipes/496702/
13from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
15import re 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
16from typing import Any, Callable, NoReturn, cast 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
19class TempliteSyntaxError(ValueError): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
20 """Raised when a template has a syntax error."""
22 pass 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
25class TempliteValueError(ValueError): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
26 """Raised when an expression won't evaluate in a template."""
28 pass 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
31class CodeBuilder: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
32 """Build source code conveniently."""
34 def __init__(self, indent: int = 0) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
35 self.code: list[str | CodeBuilder] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
36 self.indent_level = indent 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
38 def __str__(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
39 return "".join(str(c) for c in self.code) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
41 def add_line(self, line: str) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
42 """Add a line of source to the code.
44 Indentation and newline will be added for you, don't provide them.
46 """
47 self.code.extend([" " * self.indent_level, line, "\n"]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
49 def add_section(self) -> CodeBuilder: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
50 """Add a section, a sub-CodeBuilder."""
51 section = CodeBuilder(self.indent_level) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
52 self.code.append(section) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
53 return section 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
55 INDENT_STEP = 4 # PEP8 says so! 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
57 def indent(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
58 """Increase the current indent for following lines."""
59 self.indent_level += self.INDENT_STEP 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
61 def dedent(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
62 """Decrease the current indent for following lines."""
63 self.indent_level -= self.INDENT_STEP 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
65 def get_globals(self) -> dict[str, Any]: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
66 """Execute the code, and return a dict of globals it defines."""
67 # A check that the caller really finished all the blocks they started.
68 assert self.indent_level == 0 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
69 # Get the Python source as a single string.
70 python_source = str(self) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
71 # Execute the source, defining globals, and return them.
72 global_namespace: dict[str, Any] = {} 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
73 exec(python_source, global_namespace) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
74 return global_namespace 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
77class Templite: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
78 """A simple template renderer, for a nano-subset of Django syntax.
80 Supported constructs are extended variable access::
82 {{var.modifier.modifier|filter|filter}}
84 loops::
86 {% for var in list %}...{% endfor %}
88 and ifs::
90 {% if var %}...{% endif %}
92 if-else::
94 {% if var %}...{% else %}...{% endif %}
96 Comments are within curly-hash markers::
98 {# This will be ignored #}
100 Lines between `{% joined %}` and `{% endjoined %}` will have lines stripped
101 and joined. Be careful, this could join words together!
103 Any of these constructs can have a hyphen at the end (`-}}`, `-%}`, `-#}`),
104 which will collapse the white space following the tag.
106 Construct a Templite with the template text, then use `render` against a
107 dictionary context to create a finished string::
109 templite = Templite('''
110 <h1>Hello {{name|upper}}!</h1>
111 {% for topic in topics %}
112 <p>You are interested in {{topic}}.</p>
113 {% endif %}
114 ''',
115 {"upper": str.upper},
116 )
117 text = templite.render({
118 "name": "Ned",
119 "topics": ["Python", "Geometry", "Juggling"],
120 })
122 """
124 def __init__(self, text: str, *contexts: dict[str, Any]) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
125 """Construct a Templite with the given `text`.
127 `contexts` are dictionaries of values to use for future renderings.
128 These are good for filters and global values.
130 """
131 self.context = {} 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
132 for context in contexts: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
133 self.context.update(context) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
135 self.all_vars: set[str] = set() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
136 self.loop_vars: set[str] = set() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
138 # We construct a function in source form, then compile it and hold onto
139 # it, and execute it to render the template.
140 code = CodeBuilder() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
142 code.add_line("def render_function(context, do_dots):") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
143 code.indent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
144 vars_code = code.add_section() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
145 code.add_line("result = []") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
146 code.add_line("append_result = result.append") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
147 code.add_line("extend_result = result.extend") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
148 code.add_line("to_str = str") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
150 buffered: list[str] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
152 def flush_output() -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
153 """Force `buffered` to the code builder."""
154 if len(buffered) == 1: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
155 code.add_line("append_result(%s)" % buffered[0]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
156 elif len(buffered) > 1: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
157 code.add_line("extend_result([%s])" % ", ".join(buffered)) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
158 del buffered[:] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
160 ops_stack = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
162 # Split the text to form a list of tokens.
163 tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
165 squash = in_joined = False 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
167 for token in tokens: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
168 if token.startswith("{"): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
169 start, end = 2, -2 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
170 squash = (token[-3] == "-") # fmt: skip 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
171 if squash: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
172 end = -3 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
174 if token.startswith("{#"): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
175 # Comment: ignore it and move on.
176 continue 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
177 elif token.startswith("{{"): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
178 # An expression to evaluate.
179 expr = self._expr_code(token[start:end].strip()) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
180 buffered.append("to_str(%s)" % expr) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
181 else:
182 # token.startswith("{%")
183 # Action tag: split into words and parse further.
184 flush_output() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
186 words = token[start:end].strip().split() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
187 if words[0] == "if": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
188 # An if statement: evaluate the expression to determine if.
189 if len(words) != 2: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
190 self._syntax_error("Don't understand if", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
191 ops_stack.append("if") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
192 code.add_line("if %s:" % self._expr_code(words[1])) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
193 code.indent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
194 elif words[0] == "else": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
195 if len(words) != 1: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
196 self._syntax_error("Don't understand else", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
197 if not ops_stack or ops_stack[-1] != "if": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
198 self._syntax_error("Mismatched else", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
199 code.dedent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
200 code.add_line("else:") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
201 code.indent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
202 elif words[0] == "for": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
203 # A loop: iterate over expression result.
204 if len(words) != 4 or words[2] != "in": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
205 self._syntax_error("Don't understand for", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
206 ops_stack.append("for") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
207 self._variable(words[1], self.loop_vars) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
208 code.add_line( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
209 f"for c_{words[1]} in {self._expr_code(words[3])}:",
210 )
211 code.indent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
212 elif words[0] == "joined": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
213 ops_stack.append("joined") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
214 in_joined = True 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
215 elif words[0].startswith("end"): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
216 # Endsomething. Pop the ops stack.
217 if len(words) != 1: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
218 self._syntax_error("Don't understand end", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
219 end_what = words[0][3:] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
220 if not ops_stack: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
221 self._syntax_error("Too many ends", token) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN5OPQRSTUVWXYZ01234
222 start_what = ops_stack.pop() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
223 if start_what != end_what: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
224 self._syntax_error("Mismatched end tag", end_what) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN5OPQRSTUVWXYZ01234
225 if end_what == "joined": 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
226 in_joined = False 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
227 else:
228 code.dedent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
229 else:
230 self._syntax_error("Don't understand tag", words[0]) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
231 else:
232 # Literal content. If it isn't empty, output it.
233 if in_joined: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
234 token = re.sub(r"\s*\n\s*", "", token.strip()) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
235 elif squash: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
236 token = token.lstrip() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
237 if token: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
238 buffered.append(repr(token)) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
240 if ops_stack: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
241 self._syntax_error("Unmatched action tag", ops_stack[-1]) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN5OPQRSTUVWXYZ01234
243 flush_output() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
245 for var_name in self.all_vars - self.loop_vars: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
246 vars_code.add_line(f"c_{var_name} = context[{var_name!r}]") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
248 code.add_line("return ''.join(result)") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
249 code.dedent() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
250 self._render_function = cast( 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
251 Callable[
252 [dict[str, Any], Callable[..., Any]],
253 str,
254 ],
255 code.get_globals()["render_function"],
256 )
258 def _expr_code(self, expr: str) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
259 """Generate a Python expression for `expr`."""
260 if "|" in expr: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
261 pipes = expr.split("|") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
262 code = self._expr_code(pipes[0]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
263 for func in pipes[1:]: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
264 self._variable(func, self.all_vars) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
265 code = f"c_{func}({code})" 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
266 elif "." in expr: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
267 dots = expr.split(".") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
268 code = self._expr_code(dots[0]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
269 args = ", ".join(repr(d) for d in dots[1:]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
270 code = f"do_dots({code}, {args})" 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
271 else:
272 self._variable(expr, self.all_vars) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
273 code = "c_%s" % expr 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
274 return code 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
276 def _syntax_error(self, msg: str, thing: Any) -> NoReturn: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
277 """Raise a syntax error using `msg`, and showing `thing`."""
278 raise TempliteSyntaxError(f"{msg}: {thing!r}") 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
280 def _variable(self, name: str, vars_set: set[str]) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
281 """Track that `name` is used as a variable.
283 Adds the name to `vars_set`, a set of variable names.
285 Raises an syntax error if `name` is not a valid name.
287 """
288 if not re.match(r"[_a-zA-Z][_a-zA-Z0-9]*$", name): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
289 self._syntax_error("Not a valid name", name) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
290 vars_set.add(name) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
292 def render(self, context: dict[str, Any] | None = None) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
293 """Render this template by applying it to `context`.
295 `context` is a dictionary of values to use in this rendering.
297 """
298 # Make the complete context we'll use.
299 render_context = dict(self.context) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
300 if context: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
301 render_context.update(context) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
302 return self._render_function(render_context, self._do_dots) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
304 def _do_dots(self, value: Any, *dots: str) -> Any: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
305 """Evaluate dotted expressions at run-time."""
306 for dot in dots: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
307 try: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
308 value = getattr(value, dot) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
309 except AttributeError: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
310 try: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
311 value = value[dot] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
312 except (TypeError, KeyError) as exc: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
313 raise TempliteValueError( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
314 f"Couldn't evaluate {value!r}.{dot}",
315 ) from exc
316 if callable(value): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234
317 value = value() 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN5OPQRSTUVWXYZ01234
318 return value 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234