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

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 

3 

4"""A simple Python template renderer, for a nano-subset of Django syntax. 

5 

6For a detailed discussion of this code, see this chapter from 500 Lines: 

7http://aosabook.org/en/500L/a-template-engine.html 

8 

9""" 

10 

11# Coincidentally named the same as http://code.activestate.com/recipes/496702/ 

12 

13from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

14 

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

17 

18 

19class TempliteSyntaxError(ValueError): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

20 """Raised when a template has a syntax error.""" 

21 

22 pass 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

23 

24 

25class TempliteValueError(ValueError): 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

26 """Raised when an expression won't evaluate in a template.""" 

27 

28 pass 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

29 

30 

31class CodeBuilder: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

32 """Build source code conveniently.""" 

33 

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

37 

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

40 

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. 

43 

44 Indentation and newline will be added for you, don't provide them. 

45 

46 """ 

47 self.code.extend([" " * self.indent_level, line, "\n"]) 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

48 

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

54 

55 INDENT_STEP = 4 # PEP8 says so! 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

56 

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

60 

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

64 

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

75 

76 

77class Templite: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

78 """A simple template renderer, for a nano-subset of Django syntax. 

79 

80 Supported constructs are extended variable access:: 

81 

82 {{var.modifier.modifier|filter|filter}} 

83 

84 loops:: 

85 

86 {% for var in list %}...{% endfor %} 

87 

88 and ifs:: 

89 

90 {% if var %}...{% endif %} 

91 

92 if-else:: 

93 

94 {% if var %}...{% else %}...{% endif %} 

95 

96 Comments are within curly-hash markers:: 

97 

98 {# This will be ignored #} 

99 

100 Lines between `{% joined %}` and `{% endjoined %}` will have lines stripped 

101 and joined. Be careful, this could join words together! 

102 

103 Any of these constructs can have a hyphen at the end (`-}}`, `-%}`, `-#}`), 

104 which will collapse the white space following the tag. 

105 

106 Construct a Templite with the template text, then use `render` against a 

107 dictionary context to create a finished string:: 

108 

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

121 

122 """ 

123 

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

126 

127 `contexts` are dictionaries of values to use for future renderings. 

128 These are good for filters and global values. 

129 

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

134 

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

137 

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

141 

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

149 

150 buffered: list[str] = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

151 

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

159 

160 ops_stack = [] 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

161 

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

164 

165 squash = in_joined = False 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

166 

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

173 

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

185 

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

239 

240 if ops_stack: 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

241 self._syntax_error("Unmatched action tag", ops_stack[-1]) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN5OPQRSTUVWXYZ01234

242 

243 flush_output() 1abcdefghijklmnopqrstuvwxyzABCDEF6GH7IJ8KL9MN5OP!QR#ST$UV%WX'YZ(01)234

244 

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

247 

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 ) 

257 

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

275 

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

279 

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. 

282 

283 Adds the name to `vars_set`, a set of variable names. 

284 

285 Raises an syntax error if `name` is not a valid name. 

286 

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

291 

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

294 

295 `context` is a dictionary of values to use in this rendering. 

296 

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

303 

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