om@3
|
1 |
"""
|
om@3
|
2 |
simple, elegant templating
|
om@3
|
3 |
(part of web.py)
|
om@3
|
4 |
|
om@3
|
5 |
Template design:
|
om@3
|
6 |
|
om@3
|
7 |
Template string is split into tokens and the tokens are combined into nodes.
|
om@3
|
8 |
Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and
|
om@3
|
9 |
for-loop, if-loop etc are block nodes, which contain multiple child nodes.
|
om@3
|
10 |
|
om@3
|
11 |
Each node can emit some python string. python string emitted by the
|
om@3
|
12 |
root node is validated for safeeval and executed using python in the given environment.
|
om@3
|
13 |
|
om@3
|
14 |
Enough care is taken to make sure the generated code and the template has line to line match,
|
om@3
|
15 |
so that the error messages can point to exact line number in template. (It doesn't work in some cases still.)
|
om@3
|
16 |
|
om@3
|
17 |
Grammar:
|
om@3
|
18 |
|
om@3
|
19 |
template -> defwith sections
|
om@3
|
20 |
defwith -> '$def with (' arguments ')' | ''
|
om@3
|
21 |
sections -> section*
|
om@3
|
22 |
section -> block | assignment | line
|
om@3
|
23 |
|
om@3
|
24 |
assignment -> '$ ' <assignment expression>
|
om@3
|
25 |
line -> (text|expr)*
|
om@3
|
26 |
text -> <any characters other than $>
|
om@3
|
27 |
expr -> '$' pyexpr | '$(' pyexpr ')' | '${' pyexpr '}'
|
om@3
|
28 |
pyexpr -> <python expression>
|
om@3
|
29 |
"""
|
om@3
|
30 |
|
om@3
|
31 |
__all__ = [
|
om@3
|
32 |
"Template",
|
om@3
|
33 |
"Render", "render", "frender",
|
om@3
|
34 |
"ParseError", "SecurityError",
|
om@3
|
35 |
"test"
|
om@3
|
36 |
]
|
om@3
|
37 |
|
om@3
|
38 |
import tokenize
|
om@3
|
39 |
import os
|
om@3
|
40 |
import sys
|
om@3
|
41 |
import glob
|
om@3
|
42 |
import re
|
om@3
|
43 |
from UserDict import DictMixin
|
om@3
|
44 |
import warnings
|
om@3
|
45 |
|
om@3
|
46 |
from utils import storage, safeunicode, safestr, re_compile
|
om@3
|
47 |
from webapi import config
|
om@3
|
48 |
from net import websafe
|
om@3
|
49 |
|
om@3
|
50 |
def splitline(text):
|
om@3
|
51 |
r"""
|
om@3
|
52 |
Splits the given text at newline.
|
om@3
|
53 |
|
om@3
|
54 |
>>> splitline('foo\nbar')
|
om@3
|
55 |
('foo\n', 'bar')
|
om@3
|
56 |
>>> splitline('foo')
|
om@3
|
57 |
('foo', '')
|
om@3
|
58 |
>>> splitline('')
|
om@3
|
59 |
('', '')
|
om@3
|
60 |
"""
|
om@3
|
61 |
index = text.find('\n') + 1
|
om@3
|
62 |
if index:
|
om@3
|
63 |
return text[:index], text[index:]
|
om@3
|
64 |
else:
|
om@3
|
65 |
return text, ''
|
om@3
|
66 |
|
om@3
|
67 |
class Parser:
|
om@3
|
68 |
"""Parser Base.
|
om@3
|
69 |
"""
|
om@3
|
70 |
def __init__(self):
|
om@3
|
71 |
self.statement_nodes = STATEMENT_NODES
|
om@3
|
72 |
self.keywords = KEYWORDS
|
om@3
|
73 |
|
om@3
|
74 |
def parse(self, text, name="<template>"):
|
om@3
|
75 |
self.text = text
|
om@3
|
76 |
self.name = name
|
om@3
|
77 |
|
om@3
|
78 |
defwith, text = self.read_defwith(text)
|
om@3
|
79 |
suite = self.read_suite(text)
|
om@3
|
80 |
return DefwithNode(defwith, suite)
|
om@3
|
81 |
|
om@3
|
82 |
def read_defwith(self, text):
|
om@3
|
83 |
if text.startswith('$def with'):
|
om@3
|
84 |
defwith, text = splitline(text)
|
om@3
|
85 |
defwith = defwith[1:].strip() # strip $ and spaces
|
om@3
|
86 |
return defwith, text
|
om@3
|
87 |
else:
|
om@3
|
88 |
return '', text
|
om@3
|
89 |
|
om@3
|
90 |
def read_section(self, text):
|
om@3
|
91 |
r"""Reads one section from the given text.
|
om@3
|
92 |
|
om@3
|
93 |
section -> block | assignment | line
|
om@3
|
94 |
|
om@3
|
95 |
>>> read_section = Parser().read_section
|
om@3
|
96 |
>>> read_section('foo\nbar\n')
|
om@3
|
97 |
(<line: [t'foo\n']>, 'bar\n')
|
om@3
|
98 |
>>> read_section('$ a = b + 1\nfoo\n')
|
om@3
|
99 |
(<assignment: 'a = b + 1'>, 'foo\n')
|
om@3
|
100 |
|
om@3
|
101 |
read_section('$for in range(10):\n hello $i\nfoo)
|
om@3
|
102 |
"""
|
om@3
|
103 |
if text.lstrip(' ').startswith('$'):
|
om@3
|
104 |
index = text.index('$')
|
om@3
|
105 |
begin_indent, text2 = text[:index], text[index+1:]
|
om@3
|
106 |
ahead = self.python_lookahead(text2)
|
om@3
|
107 |
|
om@3
|
108 |
if ahead == 'var':
|
om@3
|
109 |
return self.read_var(text2)
|
om@3
|
110 |
elif ahead in self.statement_nodes:
|
om@3
|
111 |
return self.read_block_section(text2, begin_indent)
|
om@3
|
112 |
elif ahead in self.keywords:
|
om@3
|
113 |
return self.read_keyword(text2)
|
om@3
|
114 |
elif ahead.strip() == '':
|
om@3
|
115 |
# assignments starts with a space after $
|
om@3
|
116 |
# ex: $ a = b + 2
|
om@3
|
117 |
return self.read_assignment(text2)
|
om@3
|
118 |
return self.readline(text)
|
om@3
|
119 |
|
om@3
|
120 |
def read_var(self, text):
|
om@3
|
121 |
r"""Reads a var statement.
|
om@3
|
122 |
|
om@3
|
123 |
>>> read_var = Parser().read_var
|
om@3
|
124 |
>>> read_var('var x=10\nfoo')
|
om@3
|
125 |
(<var: x = 10>, 'foo')
|
om@3
|
126 |
>>> read_var('var x: hello $name\nfoo')
|
om@3
|
127 |
(<var: x = join_(u'hello ', escape_(name, True))>, 'foo')
|
om@3
|
128 |
"""
|
om@3
|
129 |
line, text = splitline(text)
|
om@3
|
130 |
tokens = self.python_tokens(line)
|
om@3
|
131 |
if len(tokens) < 4:
|
om@3
|
132 |
raise SyntaxError('Invalid var statement')
|
om@3
|
133 |
|
om@3
|
134 |
name = tokens[1]
|
om@3
|
135 |
sep = tokens[2]
|
om@3
|
136 |
value = line.split(sep, 1)[1].strip()
|
om@3
|
137 |
|
om@3
|
138 |
if sep == '=':
|
om@3
|
139 |
pass # no need to process value
|
om@3
|
140 |
elif sep == ':':
|
om@3
|
141 |
#@@ Hack for backward-compatability
|
om@3
|
142 |
if tokens[3] == '\n': # multi-line var statement
|
om@3
|
143 |
block, text = self.read_indented_block(text, ' ')
|
om@3
|
144 |
lines = [self.readline(x)[0] for x in block.splitlines()]
|
om@3
|
145 |
nodes = []
|
om@3
|
146 |
for x in lines:
|
om@3
|
147 |
nodes.extend(x.nodes)
|
om@3
|
148 |
nodes.append(TextNode('\n'))
|
om@3
|
149 |
else: # single-line var statement
|
om@3
|
150 |
linenode, _ = self.readline(value)
|
om@3
|
151 |
nodes = linenode.nodes
|
om@3
|
152 |
parts = [node.emit('') for node in nodes]
|
om@3
|
153 |
value = "join_(%s)" % ", ".join(parts)
|
om@3
|
154 |
else:
|
om@3
|
155 |
raise SyntaxError('Invalid var statement')
|
om@3
|
156 |
return VarNode(name, value), text
|
om@3
|
157 |
|
om@3
|
158 |
def read_suite(self, text):
|
om@3
|
159 |
r"""Reads section by section till end of text.
|
om@3
|
160 |
|
om@3
|
161 |
>>> read_suite = Parser().read_suite
|
om@3
|
162 |
>>> read_suite('hello $name\nfoo\n')
|
om@3
|
163 |
[<line: [t'hello ', $name, t'\n']>, <line: [t'foo\n']>]
|
om@3
|
164 |
"""
|
om@3
|
165 |
sections = []
|
om@3
|
166 |
while text:
|
om@3
|
167 |
section, text = self.read_section(text)
|
om@3
|
168 |
sections.append(section)
|
om@3
|
169 |
return SuiteNode(sections)
|
om@3
|
170 |
|
om@3
|
171 |
def readline(self, text):
|
om@3
|
172 |
r"""Reads one line from the text. Newline is supressed if the line ends with \.
|
om@3
|
173 |
|
om@3
|
174 |
>>> readline = Parser().readline
|
om@3
|
175 |
>>> readline('hello $name!\nbye!')
|
om@3
|
176 |
(<line: [t'hello ', $name, t'!\n']>, 'bye!')
|
om@3
|
177 |
>>> readline('hello $name!\\\nbye!')
|
om@3
|
178 |
(<line: [t'hello ', $name, t'!']>, 'bye!')
|
om@3
|
179 |
>>> readline('$f()\n\n')
|
om@3
|
180 |
(<line: [$f(), t'\n']>, '\n')
|
om@3
|
181 |
"""
|
om@3
|
182 |
line, text = splitline(text)
|
om@3
|
183 |
|
om@3
|
184 |
# supress new line if line ends with \
|
om@3
|
185 |
if line.endswith('\\\n'):
|
om@3
|
186 |
line = line[:-2]
|
om@3
|
187 |
|
om@3
|
188 |
nodes = []
|
om@3
|
189 |
while line:
|
om@3
|
190 |
node, line = self.read_node(line)
|
om@3
|
191 |
nodes.append(node)
|
om@3
|
192 |
|
om@3
|
193 |
return LineNode(nodes), text
|
om@3
|
194 |
|
om@3
|
195 |
def read_node(self, text):
|
om@3
|
196 |
r"""Reads a node from the given text and returns the node and remaining text.
|
om@3
|
197 |
|
om@3
|
198 |
>>> read_node = Parser().read_node
|
om@3
|
199 |
>>> read_node('hello $name')
|
om@3
|
200 |
(t'hello ', '$name')
|
om@3
|
201 |
>>> read_node('$name')
|
om@3
|
202 |
($name, '')
|
om@3
|
203 |
"""
|
om@3
|
204 |
if text.startswith('$$'):
|
om@3
|
205 |
return TextNode('$'), text[2:]
|
om@3
|
206 |
elif text.startswith('$#'): # comment
|
om@3
|
207 |
line, text = splitline(text)
|
om@3
|
208 |
return TextNode('\n'), text
|
om@3
|
209 |
elif text.startswith('$'):
|
om@3
|
210 |
text = text[1:] # strip $
|
om@3
|
211 |
if text.startswith(':'):
|
om@3
|
212 |
escape = False
|
om@3
|
213 |
text = text[1:] # strip :
|
om@3
|
214 |
else:
|
om@3
|
215 |
escape = True
|
om@3
|
216 |
return self.read_expr(text, escape=escape)
|
om@3
|
217 |
else:
|
om@3
|
218 |
return self.read_text(text)
|
om@3
|
219 |
|
om@3
|
220 |
def read_text(self, text):
|
om@3
|
221 |
r"""Reads a text node from the given text.
|
om@3
|
222 |
|
om@3
|
223 |
>>> read_text = Parser().read_text
|
om@3
|
224 |
>>> read_text('hello $name')
|
om@3
|
225 |
(t'hello ', '$name')
|
om@3
|
226 |
"""
|
om@3
|
227 |
index = text.find('$')
|
om@3
|
228 |
if index < 0:
|
om@3
|
229 |
return TextNode(text), ''
|
om@3
|
230 |
else:
|
om@3
|
231 |
return TextNode(text[:index]), text[index:]
|
om@3
|
232 |
|
om@3
|
233 |
def read_keyword(self, text):
|
om@3
|
234 |
line, text = splitline(text)
|
om@3
|
235 |
return StatementNode(line.strip() + "\n"), text
|
om@3
|
236 |
|
om@3
|
237 |
def read_expr(self, text, escape=True):
|
om@3
|
238 |
"""Reads a python expression from the text and returns the expression and remaining text.
|
om@3
|
239 |
|
om@3
|
240 |
expr -> simple_expr | paren_expr
|
om@3
|
241 |
simple_expr -> id extended_expr
|
om@3
|
242 |
extended_expr -> attr_access | paren_expr extended_expr | ''
|
om@3
|
243 |
attr_access -> dot id extended_expr
|
om@3
|
244 |
paren_expr -> [ tokens ] | ( tokens ) | { tokens }
|
om@3
|
245 |
|
om@3
|
246 |
>>> read_expr = Parser().read_expr
|
om@3
|
247 |
>>> read_expr("name")
|
om@3
|
248 |
($name, '')
|
om@3
|
249 |
>>> read_expr("a.b and c")
|
om@3
|
250 |
($a.b, ' and c')
|
om@3
|
251 |
>>> read_expr("a. b")
|
om@3
|
252 |
($a, '. b')
|
om@3
|
253 |
>>> read_expr("name</h1>")
|
om@3
|
254 |
($name, '</h1>')
|
om@3
|
255 |
>>> read_expr("(limit)ing")
|
om@3
|
256 |
($(limit), 'ing')
|
om@3
|
257 |
>>> read_expr('a[1, 2][:3].f(1+2, "weird string[).", 3 + 4) done.')
|
om@3
|
258 |
($a[1, 2][:3].f(1+2, "weird string[).", 3 + 4), ' done.')
|
om@3
|
259 |
"""
|
om@3
|
260 |
def simple_expr():
|
om@3
|
261 |
identifier()
|
om@3
|
262 |
extended_expr()
|
om@3
|
263 |
|
om@3
|
264 |
def identifier():
|
om@3
|
265 |
tokens.next()
|
om@3
|
266 |
|
om@3
|
267 |
def extended_expr():
|
om@3
|
268 |
lookahead = tokens.lookahead()
|
om@3
|
269 |
if lookahead is None:
|
om@3
|
270 |
return
|
om@3
|
271 |
elif lookahead.value == '.':
|
om@3
|
272 |
attr_access()
|
om@3
|
273 |
elif lookahead.value in parens:
|
om@3
|
274 |
paren_expr()
|
om@3
|
275 |
extended_expr()
|
om@3
|
276 |
else:
|
om@3
|
277 |
return
|
om@3
|
278 |
|
om@3
|
279 |
def attr_access():
|
om@3
|
280 |
from token import NAME # python token constants
|
om@3
|
281 |
dot = tokens.lookahead()
|
om@3
|
282 |
if tokens.lookahead2().type == NAME:
|
om@3
|
283 |
tokens.next() # consume dot
|
om@3
|
284 |
identifier()
|
om@3
|
285 |
extended_expr()
|
om@3
|
286 |
|
om@3
|
287 |
def paren_expr():
|
om@3
|
288 |
begin = tokens.next().value
|
om@3
|
289 |
end = parens[begin]
|
om@3
|
290 |
while True:
|
om@3
|
291 |
if tokens.lookahead().value in parens:
|
om@3
|
292 |
paren_expr()
|
om@3
|
293 |
else:
|
om@3
|
294 |
t = tokens.next()
|
om@3
|
295 |
if t.value == end:
|
om@3
|
296 |
break
|
om@3
|
297 |
return
|
om@3
|
298 |
|
om@3
|
299 |
parens = {
|
om@3
|
300 |
"(": ")",
|
om@3
|
301 |
"[": "]",
|
om@3
|
302 |
"{": "}"
|
om@3
|
303 |
}
|
om@3
|
304 |
|
om@3
|
305 |
def get_tokens(text):
|
om@3
|
306 |
"""tokenize text using python tokenizer.
|
om@3
|
307 |
Python tokenizer ignores spaces, but they might be important in some cases.
|
om@3
|
308 |
This function introduces dummy space tokens when it identifies any ignored space.
|
om@3
|
309 |
Each token is a storage object containing type, value, begin and end.
|
om@3
|
310 |
"""
|
om@3
|
311 |
readline = iter([text]).next
|
om@3
|
312 |
end = None
|
om@3
|
313 |
for t in tokenize.generate_tokens(readline):
|
om@3
|
314 |
t = storage(type=t[0], value=t[1], begin=t[2], end=t[3])
|
om@3
|
315 |
if end is not None and end != t.begin:
|
om@3
|
316 |
_, x1 = end
|
om@3
|
317 |
_, x2 = t.begin
|
om@3
|
318 |
yield storage(type=-1, value=text[x1:x2], begin=end, end=t.begin)
|
om@3
|
319 |
end = t.end
|
om@3
|
320 |
yield t
|
om@3
|
321 |
|
om@3
|
322 |
class BetterIter:
|
om@3
|
323 |
"""Iterator like object with 2 support for 2 look aheads."""
|
om@3
|
324 |
def __init__(self, items):
|
om@3
|
325 |
self.iteritems = iter(items)
|
om@3
|
326 |
self.items = []
|
om@3
|
327 |
self.position = 0
|
om@3
|
328 |
self.current_item = None
|
om@3
|
329 |
|
om@3
|
330 |
def lookahead(self):
|
om@3
|
331 |
if len(self.items) <= self.position:
|
om@3
|
332 |
self.items.append(self._next())
|
om@3
|
333 |
return self.items[self.position]
|
om@3
|
334 |
|
om@3
|
335 |
def _next(self):
|
om@3
|
336 |
try:
|
om@3
|
337 |
return self.iteritems.next()
|
om@3
|
338 |
except StopIteration:
|
om@3
|
339 |
return None
|
om@3
|
340 |
|
om@3
|
341 |
def lookahead2(self):
|
om@3
|
342 |
if len(self.items) <= self.position+1:
|
om@3
|
343 |
self.items.append(self._next())
|
om@3
|
344 |
return self.items[self.position+1]
|
om@3
|
345 |
|
om@3
|
346 |
def next(self):
|
om@3
|
347 |
self.current_item = self.lookahead()
|
om@3
|
348 |
self.position += 1
|
om@3
|
349 |
return self.current_item
|
om@3
|
350 |
|
om@3
|
351 |
tokens = BetterIter(get_tokens(text))
|
om@3
|
352 |
|
om@3
|
353 |
if tokens.lookahead().value in parens:
|
om@3
|
354 |
paren_expr()
|
om@3
|
355 |
else:
|
om@3
|
356 |
simple_expr()
|
om@3
|
357 |
row, col = tokens.current_item.end
|
om@3
|
358 |
return ExpressionNode(text[:col], escape=escape), text[col:]
|
om@3
|
359 |
|
om@3
|
360 |
def read_assignment(self, text):
|
om@3
|
361 |
r"""Reads assignment statement from text.
|
om@3
|
362 |
|
om@3
|
363 |
>>> read_assignment = Parser().read_assignment
|
om@3
|
364 |
>>> read_assignment('a = b + 1\nfoo')
|
om@3
|
365 |
(<assignment: 'a = b + 1'>, 'foo')
|
om@3
|
366 |
"""
|
om@3
|
367 |
line, text = splitline(text)
|
om@3
|
368 |
return AssignmentNode(line.strip()), text
|
om@3
|
369 |
|
om@3
|
370 |
def python_lookahead(self, text):
|
om@3
|
371 |
"""Returns the first python token from the given text.
|
om@3
|
372 |
|
om@3
|
373 |
>>> python_lookahead = Parser().python_lookahead
|
om@3
|
374 |
>>> python_lookahead('for i in range(10):')
|
om@3
|
375 |
'for'
|
om@3
|
376 |
>>> python_lookahead('else:')
|
om@3
|
377 |
'else'
|
om@3
|
378 |
>>> python_lookahead(' x = 1')
|
om@3
|
379 |
' '
|
om@3
|
380 |
"""
|
om@3
|
381 |
readline = iter([text]).next
|
om@3
|
382 |
tokens = tokenize.generate_tokens(readline)
|
om@3
|
383 |
return tokens.next()[1]
|
om@3
|
384 |
|
om@3
|
385 |
def python_tokens(self, text):
|
om@3
|
386 |
readline = iter([text]).next
|
om@3
|
387 |
tokens = tokenize.generate_tokens(readline)
|
om@3
|
388 |
return [t[1] for t in tokens]
|
om@3
|
389 |
|
om@3
|
390 |
def read_indented_block(self, text, indent):
|
om@3
|
391 |
r"""Read a block of text. A block is what typically follows a for or it statement.
|
om@3
|
392 |
It can be in the same line as that of the statement or an indented block.
|
om@3
|
393 |
|
om@3
|
394 |
>>> read_indented_block = Parser().read_indented_block
|
om@3
|
395 |
>>> read_indented_block(' a\n b\nc', ' ')
|
om@3
|
396 |
('a\nb\n', 'c')
|
om@3
|
397 |
>>> read_indented_block(' a\n b\n c\nd', ' ')
|
om@3
|
398 |
('a\n b\nc\n', 'd')
|
om@3
|
399 |
>>> read_indented_block(' a\n\n b\nc', ' ')
|
om@3
|
400 |
('a\n\n b\n', 'c')
|
om@3
|
401 |
"""
|
om@3
|
402 |
if indent == '':
|
om@3
|
403 |
return '', text
|
om@3
|
404 |
|
om@3
|
405 |
block = ""
|
om@3
|
406 |
while text:
|
om@3
|
407 |
line, text2 = splitline(text)
|
om@3
|
408 |
if line.strip() == "":
|
om@3
|
409 |
block += '\n'
|
om@3
|
410 |
elif line.startswith(indent):
|
om@3
|
411 |
block += line[len(indent):]
|
om@3
|
412 |
else:
|
om@3
|
413 |
break
|
om@3
|
414 |
text = text2
|
om@3
|
415 |
return block, text
|
om@3
|
416 |
|
om@3
|
417 |
def read_statement(self, text):
|
om@3
|
418 |
r"""Reads a python statement.
|
om@3
|
419 |
|
om@3
|
420 |
>>> read_statement = Parser().read_statement
|
om@3
|
421 |
>>> read_statement('for i in range(10): hello $name')
|
om@3
|
422 |
('for i in range(10):', ' hello $name')
|
om@3
|
423 |
"""
|
om@3
|
424 |
tok = PythonTokenizer(text)
|
om@3
|
425 |
tok.consume_till(':')
|
om@3
|
426 |
return text[:tok.index], text[tok.index:]
|
om@3
|
427 |
|
om@3
|
428 |
def read_block_section(self, text, begin_indent=''):
|
om@3
|
429 |
r"""
|
om@3
|
430 |
>>> read_block_section = Parser().read_block_section
|
om@3
|
431 |
>>> read_block_section('for i in range(10): hello $i\nfoo')
|
om@3
|
432 |
(<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
|
om@3
|
433 |
>>> read_block_section('for i in range(10):\n hello $i\n foo', begin_indent=' ')
|
om@3
|
434 |
(<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, ' foo')
|
om@3
|
435 |
>>> read_block_section('for i in range(10):\n hello $i\nfoo')
|
om@3
|
436 |
(<block: 'for i in range(10):', [<line: [t'hello ', $i, t'\n']>]>, 'foo')
|
om@3
|
437 |
"""
|
om@3
|
438 |
line, text = splitline(text)
|
om@3
|
439 |
stmt, line = self.read_statement(line)
|
om@3
|
440 |
keyword = self.python_lookahead(stmt)
|
om@3
|
441 |
|
om@3
|
442 |
# if there is some thing left in the line
|
om@3
|
443 |
if line.strip():
|
om@3
|
444 |
block = line.lstrip()
|
om@3
|
445 |
else:
|
om@3
|
446 |
def find_indent(text):
|
om@3
|
447 |
rx = re_compile(' +')
|
om@3
|
448 |
match = rx.match(text)
|
om@3
|
449 |
first_indent = match and match.group(0)
|
om@3
|
450 |
return first_indent or ""
|
om@3
|
451 |
|
om@3
|
452 |
# find the indentation of the block by looking at the first line
|
om@3
|
453 |
first_indent = find_indent(text)[len(begin_indent):]
|
om@3
|
454 |
|
om@3
|
455 |
#TODO: fix this special case
|
om@3
|
456 |
if keyword == "code":
|
om@3
|
457 |
indent = begin_indent + first_indent
|
om@3
|
458 |
else:
|
om@3
|
459 |
indent = begin_indent + min(first_indent, INDENT)
|
om@3
|
460 |
|
om@3
|
461 |
block, text = self.read_indented_block(text, indent)
|
om@3
|
462 |
|
om@3
|
463 |
return self.create_block_node(keyword, stmt, block, begin_indent), text
|
om@3
|
464 |
|
om@3
|
465 |
def create_block_node(self, keyword, stmt, block, begin_indent):
|
om@3
|
466 |
if keyword in self.statement_nodes:
|
om@3
|
467 |
return self.statement_nodes[keyword](stmt, block, begin_indent)
|
om@3
|
468 |
else:
|
om@3
|
469 |
raise ParseError, 'Unknown statement: %s' % repr(keyword)
|
om@3
|
470 |
|
om@3
|
471 |
class PythonTokenizer:
|
om@3
|
472 |
"""Utility wrapper over python tokenizer."""
|
om@3
|
473 |
def __init__(self, text):
|
om@3
|
474 |
self.text = text
|
om@3
|
475 |
readline = iter([text]).next
|
om@3
|
476 |
self.tokens = tokenize.generate_tokens(readline)
|
om@3
|
477 |
self.index = 0
|
om@3
|
478 |
|
om@3
|
479 |
def consume_till(self, delim):
|
om@3
|
480 |
"""Consumes tokens till colon.
|
om@3
|
481 |
|
om@3
|
482 |
>>> tok = PythonTokenizer('for i in range(10): hello $i')
|
om@3
|
483 |
>>> tok.consume_till(':')
|
om@3
|
484 |
>>> tok.text[:tok.index]
|
om@3
|
485 |
'for i in range(10):'
|
om@3
|
486 |
>>> tok.text[tok.index:]
|
om@3
|
487 |
' hello $i'
|
om@3
|
488 |
"""
|
om@3
|
489 |
try:
|
om@3
|
490 |
while True:
|
om@3
|
491 |
t = self.next()
|
om@3
|
492 |
if t.value == delim:
|
om@3
|
493 |
break
|
om@3
|
494 |
elif t.value == '(':
|
om@3
|
495 |
self.consume_till(')')
|
om@3
|
496 |
elif t.value == '[':
|
om@3
|
497 |
self.consume_till(']')
|
om@3
|
498 |
elif t.value == '{':
|
om@3
|
499 |
self.consume_till('}')
|
om@3
|
500 |
|
om@3
|
501 |
# if end of line is found, it is an exception.
|
om@3
|
502 |
# Since there is no easy way to report the line number,
|
om@3
|
503 |
# leave the error reporting to the python parser later
|
om@3
|
504 |
#@@ This should be fixed.
|
om@3
|
505 |
if t.value == '\n':
|
om@3
|
506 |
break
|
om@3
|
507 |
except:
|
om@3
|
508 |
#raise ParseError, "Expected %s, found end of line." % repr(delim)
|
om@3
|
509 |
|
om@3
|
510 |
# raising ParseError doesn't show the line number.
|
om@3
|
511 |
# if this error is ignored, then it will be caught when compiling the python code.
|
om@3
|
512 |
return
|
om@3
|
513 |
|
om@3
|
514 |
def next(self):
|
om@3
|
515 |
type, t, begin, end, line = self.tokens.next()
|
om@3
|
516 |
row, col = end
|
om@3
|
517 |
self.index = col
|
om@3
|
518 |
return storage(type=type, value=t, begin=begin, end=end)
|
om@3
|
519 |
|
om@3
|
520 |
class DefwithNode:
|
om@3
|
521 |
def __init__(self, defwith, suite):
|
om@3
|
522 |
if defwith:
|
om@3
|
523 |
self.defwith = defwith.replace('with', '__template__') + ':'
|
om@3
|
524 |
# offset 4 lines. for encoding, __lineoffset__, loop and self.
|
om@3
|
525 |
self.defwith += "\n __lineoffset__ = -4"
|
om@3
|
526 |
else:
|
om@3
|
527 |
self.defwith = 'def __template__():'
|
om@3
|
528 |
# offset 4 lines for encoding, __template__, __lineoffset__, loop and self.
|
om@3
|
529 |
self.defwith += "\n __lineoffset__ = -5"
|
om@3
|
530 |
|
om@3
|
531 |
self.defwith += "\n loop = ForLoop()"
|
om@3
|
532 |
self.defwith += "\n self = TemplateResult(); extend_ = self.extend"
|
om@3
|
533 |
self.suite = suite
|
om@3
|
534 |
self.end = "\n return self"
|
om@3
|
535 |
|
om@3
|
536 |
def emit(self, indent):
|
om@3
|
537 |
encoding = "# coding: utf-8\n"
|
om@3
|
538 |
return encoding + self.defwith + self.suite.emit(indent + INDENT) + self.end
|
om@3
|
539 |
|
om@3
|
540 |
def __repr__(self):
|
om@3
|
541 |
return "<defwith: %s, %s>" % (self.defwith, self.suite)
|
om@3
|
542 |
|
om@3
|
543 |
class TextNode:
|
om@3
|
544 |
def __init__(self, value):
|
om@3
|
545 |
self.value = value
|
om@3
|
546 |
|
om@3
|
547 |
def emit(self, indent, begin_indent=''):
|
om@3
|
548 |
return repr(safeunicode(self.value))
|
om@3
|
549 |
|
om@3
|
550 |
def __repr__(self):
|
om@3
|
551 |
return 't' + repr(self.value)
|
om@3
|
552 |
|
om@3
|
553 |
class ExpressionNode:
|
om@3
|
554 |
def __init__(self, value, escape=True):
|
om@3
|
555 |
self.value = value.strip()
|
om@3
|
556 |
|
om@3
|
557 |
# convert ${...} to $(...)
|
om@3
|
558 |
if value.startswith('{') and value.endswith('}'):
|
om@3
|
559 |
self.value = '(' + self.value[1:-1] + ')'
|
om@3
|
560 |
|
om@3
|
561 |
self.escape = escape
|
om@3
|
562 |
|
om@3
|
563 |
def emit(self, indent, begin_indent=''):
|
om@3
|
564 |
return 'escape_(%s, %s)' % (self.value, bool(self.escape))
|
om@3
|
565 |
|
om@3
|
566 |
def __repr__(self):
|
om@3
|
567 |
if self.escape:
|
om@3
|
568 |
escape = ''
|
om@3
|
569 |
else:
|
om@3
|
570 |
escape = ':'
|
om@3
|
571 |
return "$%s%s" % (escape, self.value)
|
om@3
|
572 |
|
om@3
|
573 |
class AssignmentNode:
|
om@3
|
574 |
def __init__(self, code):
|
om@3
|
575 |
self.code = code
|
om@3
|
576 |
|
om@3
|
577 |
def emit(self, indent, begin_indent=''):
|
om@3
|
578 |
return indent + self.code + "\n"
|
om@3
|
579 |
|
om@3
|
580 |
def __repr__(self):
|
om@3
|
581 |
return "<assignment: %s>" % repr(self.code)
|
om@3
|
582 |
|
om@3
|
583 |
class LineNode:
|
om@3
|
584 |
def __init__(self, nodes):
|
om@3
|
585 |
self.nodes = nodes
|
om@3
|
586 |
|
om@3
|
587 |
def emit(self, indent, text_indent='', name=''):
|
om@3
|
588 |
text = [node.emit('') for node in self.nodes]
|
om@3
|
589 |
if text_indent:
|
om@3
|
590 |
text = [repr(text_indent)] + text
|
om@3
|
591 |
|
om@3
|
592 |
return indent + "extend_([%s])\n" % ", ".join(text)
|
om@3
|
593 |
|
om@3
|
594 |
def __repr__(self):
|
om@3
|
595 |
return "<line: %s>" % repr(self.nodes)
|
om@3
|
596 |
|
om@3
|
597 |
INDENT = ' ' # 4 spaces
|
om@3
|
598 |
|
om@3
|
599 |
class BlockNode:
|
om@3
|
600 |
def __init__(self, stmt, block, begin_indent=''):
|
om@3
|
601 |
self.stmt = stmt
|
om@3
|
602 |
self.suite = Parser().read_suite(block)
|
om@3
|
603 |
self.begin_indent = begin_indent
|
om@3
|
604 |
|
om@3
|
605 |
def emit(self, indent, text_indent=''):
|
om@3
|
606 |
text_indent = self.begin_indent + text_indent
|
om@3
|
607 |
out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
|
om@3
|
608 |
return out
|
om@3
|
609 |
|
om@3
|
610 |
def __repr__(self):
|
om@3
|
611 |
return "<block: %s, %s>" % (repr(self.stmt), repr(self.suite))
|
om@3
|
612 |
|
om@3
|
613 |
class ForNode(BlockNode):
|
om@3
|
614 |
def __init__(self, stmt, block, begin_indent=''):
|
om@3
|
615 |
self.original_stmt = stmt
|
om@3
|
616 |
tok = PythonTokenizer(stmt)
|
om@3
|
617 |
tok.consume_till('in')
|
om@3
|
618 |
a = stmt[:tok.index] # for i in
|
om@3
|
619 |
b = stmt[tok.index:-1] # rest of for stmt excluding :
|
om@3
|
620 |
stmt = a + ' loop.setup(' + b.strip() + '):'
|
om@3
|
621 |
BlockNode.__init__(self, stmt, block, begin_indent)
|
om@3
|
622 |
|
om@3
|
623 |
def __repr__(self):
|
om@3
|
624 |
return "<block: %s, %s>" % (repr(self.original_stmt), repr(self.suite))
|
om@3
|
625 |
|
om@3
|
626 |
class CodeNode:
|
om@3
|
627 |
def __init__(self, stmt, block, begin_indent=''):
|
om@3
|
628 |
# compensate one line for $code:
|
om@3
|
629 |
self.code = "\n" + block
|
om@3
|
630 |
|
om@3
|
631 |
def emit(self, indent, text_indent=''):
|
om@3
|
632 |
import re
|
om@3
|
633 |
rx = re.compile('^', re.M)
|
om@3
|
634 |
return rx.sub(indent, self.code).rstrip(' ')
|
om@3
|
635 |
|
om@3
|
636 |
def __repr__(self):
|
om@3
|
637 |
return "<code: %s>" % repr(self.code)
|
om@3
|
638 |
|
om@3
|
639 |
class StatementNode:
|
om@3
|
640 |
def __init__(self, stmt):
|
om@3
|
641 |
self.stmt = stmt
|
om@3
|
642 |
|
om@3
|
643 |
def emit(self, indent, begin_indent=''):
|
om@3
|
644 |
return indent + self.stmt
|
om@3
|
645 |
|
om@3
|
646 |
def __repr__(self):
|
om@3
|
647 |
return "<stmt: %s>" % repr(self.stmt)
|
om@3
|
648 |
|
om@3
|
649 |
class IfNode(BlockNode):
|
om@3
|
650 |
pass
|
om@3
|
651 |
|
om@3
|
652 |
class ElseNode(BlockNode):
|
om@3
|
653 |
pass
|
om@3
|
654 |
|
om@3
|
655 |
class ElifNode(BlockNode):
|
om@3
|
656 |
pass
|
om@3
|
657 |
|
om@3
|
658 |
class DefNode(BlockNode):
|
om@3
|
659 |
def __init__(self, *a, **kw):
|
om@3
|
660 |
BlockNode.__init__(self, *a, **kw)
|
om@3
|
661 |
|
om@3
|
662 |
code = CodeNode("", "")
|
om@3
|
663 |
code.code = "self = TemplateResult(); extend_ = self.extend\n"
|
om@3
|
664 |
self.suite.sections.insert(0, code)
|
om@3
|
665 |
|
om@3
|
666 |
code = CodeNode("", "")
|
om@3
|
667 |
code.code = "return self\n"
|
om@3
|
668 |
self.suite.sections.append(code)
|
om@3
|
669 |
|
om@3
|
670 |
def emit(self, indent, text_indent=''):
|
om@3
|
671 |
text_indent = self.begin_indent + text_indent
|
om@3
|
672 |
out = indent + self.stmt + self.suite.emit(indent + INDENT, text_indent)
|
om@3
|
673 |
return indent + "__lineoffset__ -= 3\n" + out
|
om@3
|
674 |
|
om@3
|
675 |
class VarNode:
|
om@3
|
676 |
def __init__(self, name, value):
|
om@3
|
677 |
self.name = name
|
om@3
|
678 |
self.value = value
|
om@3
|
679 |
|
om@3
|
680 |
def emit(self, indent, text_indent):
|
om@3
|
681 |
return indent + "self[%s] = %s\n" % (repr(self.name), self.value)
|
om@3
|
682 |
|
om@3
|
683 |
def __repr__(self):
|
om@3
|
684 |
return "<var: %s = %s>" % (self.name, self.value)
|
om@3
|
685 |
|
om@3
|
686 |
class SuiteNode:
|
om@3
|
687 |
"""Suite is a list of sections."""
|
om@3
|
688 |
def __init__(self, sections):
|
om@3
|
689 |
self.sections = sections
|
om@3
|
690 |
|
om@3
|
691 |
def emit(self, indent, text_indent=''):
|
om@3
|
692 |
return "\n" + "".join([s.emit(indent, text_indent) for s in self.sections])
|
om@3
|
693 |
|
om@3
|
694 |
def __repr__(self):
|
om@3
|
695 |
return repr(self.sections)
|
om@3
|
696 |
|
om@3
|
697 |
STATEMENT_NODES = {
|
om@3
|
698 |
'for': ForNode,
|
om@3
|
699 |
'while': BlockNode,
|
om@3
|
700 |
'if': IfNode,
|
om@3
|
701 |
'elif': ElifNode,
|
om@3
|
702 |
'else': ElseNode,
|
om@3
|
703 |
'def': DefNode,
|
om@3
|
704 |
'code': CodeNode
|
om@3
|
705 |
}
|
om@3
|
706 |
|
om@3
|
707 |
KEYWORDS = [
|
om@3
|
708 |
"pass",
|
om@3
|
709 |
"break",
|
om@3
|
710 |
"continue",
|
om@3
|
711 |
"return"
|
om@3
|
712 |
]
|
om@3
|
713 |
|
om@3
|
714 |
TEMPLATE_BUILTIN_NAMES = [
|
om@3
|
715 |
"dict", "enumerate", "float", "int", "bool", "list", "long", "reversed",
|
om@3
|
716 |
"set", "slice", "tuple", "xrange",
|
om@3
|
717 |
"abs", "all", "any", "callable", "chr", "cmp", "divmod", "filter", "hex",
|
om@3
|
718 |
"id", "isinstance", "iter", "len", "max", "min", "oct", "ord", "pow", "range",
|
om@3
|
719 |
"True", "False",
|
om@3
|
720 |
"None",
|
om@3
|
721 |
"__import__", # some c-libraries like datetime requires __import__ to present in the namespace
|
om@3
|
722 |
]
|
om@3
|
723 |
|
om@3
|
724 |
import __builtin__
|
om@3
|
725 |
TEMPLATE_BUILTINS = dict([(name, getattr(__builtin__, name)) for name in TEMPLATE_BUILTIN_NAMES if name in __builtin__.__dict__])
|
om@3
|
726 |
|
om@3
|
727 |
class ForLoop:
|
om@3
|
728 |
"""
|
om@3
|
729 |
Wrapper for expression in for stament to support loop.xxx helpers.
|
om@3
|
730 |
|
om@3
|
731 |
>>> loop = ForLoop()
|
om@3
|
732 |
>>> for x in loop.setup(['a', 'b', 'c']):
|
om@3
|
733 |
... print loop.index, loop.revindex, loop.parity, x
|
om@3
|
734 |
...
|
om@3
|
735 |
1 3 odd a
|
om@3
|
736 |
2 2 even b
|
om@3
|
737 |
3 1 odd c
|
om@3
|
738 |
>>> loop.index
|
om@3
|
739 |
Traceback (most recent call last):
|
om@3
|
740 |
...
|
om@3
|
741 |
AttributeError: index
|
om@3
|
742 |
"""
|
om@3
|
743 |
def __init__(self):
|
om@3
|
744 |
self._ctx = None
|
om@3
|
745 |
|
om@3
|
746 |
def __getattr__(self, name):
|
om@3
|
747 |
if self._ctx is None:
|
om@3
|
748 |
raise AttributeError, name
|
om@3
|
749 |
else:
|
om@3
|
750 |
return getattr(self._ctx, name)
|
om@3
|
751 |
|
om@3
|
752 |
def setup(self, seq):
|
om@3
|
753 |
self._push()
|
om@3
|
754 |
return self._ctx.setup(seq)
|
om@3
|
755 |
|
om@3
|
756 |
def _push(self):
|
om@3
|
757 |
self._ctx = ForLoopContext(self, self._ctx)
|
om@3
|
758 |
|
om@3
|
759 |
def _pop(self):
|
om@3
|
760 |
self._ctx = self._ctx.parent
|
om@3
|
761 |
|
om@3
|
762 |
class ForLoopContext:
|
om@3
|
763 |
"""Stackable context for ForLoop to support nested for loops.
|
om@3
|
764 |
"""
|
om@3
|
765 |
def __init__(self, forloop, parent):
|
om@3
|
766 |
self._forloop = forloop
|
om@3
|
767 |
self.parent = parent
|
om@3
|
768 |
|
om@3
|
769 |
def setup(self, seq):
|
om@3
|
770 |
try:
|
om@3
|
771 |
self.length = len(seq)
|
om@3
|
772 |
except:
|
om@3
|
773 |
self.length = 0
|
om@3
|
774 |
|
om@3
|
775 |
self.index = 0
|
om@3
|
776 |
for a in seq:
|
om@3
|
777 |
self.index += 1
|
om@3
|
778 |
yield a
|
om@3
|
779 |
self._forloop._pop()
|
om@3
|
780 |
|
om@3
|
781 |
index0 = property(lambda self: self.index-1)
|
om@3
|
782 |
first = property(lambda self: self.index == 1)
|
om@3
|
783 |
last = property(lambda self: self.index == self.length)
|
om@3
|
784 |
odd = property(lambda self: self.index % 2 == 1)
|
om@3
|
785 |
even = property(lambda self: self.index % 2 == 0)
|
om@3
|
786 |
parity = property(lambda self: ['odd', 'even'][self.even])
|
om@3
|
787 |
revindex0 = property(lambda self: self.length - self.index)
|
om@3
|
788 |
revindex = property(lambda self: self.length - self.index + 1)
|
om@3
|
789 |
|
om@3
|
790 |
class BaseTemplate:
|
om@3
|
791 |
def __init__(self, code, filename, filter, globals, builtins):
|
om@3
|
792 |
self.filename = filename
|
om@3
|
793 |
self.filter = filter
|
om@3
|
794 |
self._globals = globals
|
om@3
|
795 |
self._builtins = builtins
|
om@3
|
796 |
if code:
|
om@3
|
797 |
self.t = self._compile(code)
|
om@3
|
798 |
else:
|
om@3
|
799 |
self.t = lambda: ''
|
om@3
|
800 |
|
om@3
|
801 |
def _compile(self, code):
|
om@3
|
802 |
env = self.make_env(self._globals or {}, self._builtins)
|
om@3
|
803 |
exec(code, env)
|
om@3
|
804 |
return env['__template__']
|
om@3
|
805 |
|
om@3
|
806 |
def __call__(self, *a, **kw):
|
om@3
|
807 |
__hidetraceback__ = True
|
om@3
|
808 |
return self.t(*a, **kw)
|
om@3
|
809 |
|
om@3
|
810 |
def make_env(self, globals, builtins):
|
om@3
|
811 |
return dict(globals,
|
om@3
|
812 |
__builtins__=builtins,
|
om@3
|
813 |
ForLoop=ForLoop,
|
om@3
|
814 |
TemplateResult=TemplateResult,
|
om@3
|
815 |
escape_=self._escape,
|
om@3
|
816 |
join_=self._join
|
om@3
|
817 |
)
|
om@3
|
818 |
def _join(self, *items):
|
om@3
|
819 |
return u"".join(items)
|
om@3
|
820 |
|
om@3
|
821 |
def _escape(self, value, escape=False):
|
om@3
|
822 |
if value is None:
|
om@3
|
823 |
value = ''
|
om@3
|
824 |
|
om@3
|
825 |
value = safeunicode(value)
|
om@3
|
826 |
if escape and self.filter:
|
om@3
|
827 |
value = self.filter(value)
|
om@3
|
828 |
return value
|
om@3
|
829 |
|
om@3
|
830 |
class Template(BaseTemplate):
|
om@3
|
831 |
CONTENT_TYPES = {
|
om@3
|
832 |
'.html' : 'text/html; charset=utf-8',
|
om@3
|
833 |
'.xhtml' : 'application/xhtml+xml; charset=utf-8',
|
om@3
|
834 |
'.txt' : 'text/plain',
|
om@3
|
835 |
}
|
om@3
|
836 |
FILTERS = {
|
om@3
|
837 |
'.html': websafe,
|
om@3
|
838 |
'.xhtml': websafe,
|
om@3
|
839 |
'.xml': websafe
|
om@3
|
840 |
}
|
om@3
|
841 |
globals = {}
|
om@3
|
842 |
|
om@3
|
843 |
def __init__(self, text, filename='<template>', filter=None, globals=None, builtins=None, extensions=None):
|
om@3
|
844 |
self.extensions = extensions or []
|
om@3
|
845 |
text = Template.normalize_text(text)
|
om@3
|
846 |
code = self.compile_template(text, filename)
|
om@3
|
847 |
|
om@3
|
848 |
_, ext = os.path.splitext(filename)
|
om@3
|
849 |
filter = filter or self.FILTERS.get(ext, None)
|
om@3
|
850 |
self.content_type = self.CONTENT_TYPES.get(ext, None)
|
om@3
|
851 |
|
om@3
|
852 |
if globals is None:
|
om@3
|
853 |
globals = self.globals
|
om@3
|
854 |
if builtins is None:
|
om@3
|
855 |
builtins = TEMPLATE_BUILTINS
|
om@3
|
856 |
|
om@3
|
857 |
BaseTemplate.__init__(self, code=code, filename=filename, filter=filter, globals=globals, builtins=builtins)
|
om@3
|
858 |
|
om@3
|
859 |
def normalize_text(text):
|
om@3
|
860 |
"""Normalizes template text by correcting \r\n, tabs and BOM chars."""
|
om@3
|
861 |
text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
|
om@3
|
862 |
if not text.endswith('\n'):
|
om@3
|
863 |
text += '\n'
|
om@3
|
864 |
|
om@3
|
865 |
# ignore BOM chars at the begining of template
|
om@3
|
866 |
BOM = '\xef\xbb\xbf'
|
om@3
|
867 |
if isinstance(text, str) and text.startswith(BOM):
|
om@3
|
868 |
text = text[len(BOM):]
|
om@3
|
869 |
|
om@3
|
870 |
# support fort \$ for backward-compatibility
|
om@3
|
871 |
text = text.replace(r'\$', '$$')
|
om@3
|
872 |
return text
|
om@3
|
873 |
normalize_text = staticmethod(normalize_text)
|
om@3
|
874 |
|
om@3
|
875 |
def __call__(self, *a, **kw):
|
om@3
|
876 |
__hidetraceback__ = True
|
om@3
|
877 |
import webapi as web
|
om@3
|
878 |
if 'headers' in web.ctx and self.content_type:
|
om@3
|
879 |
web.header('Content-Type', self.content_type, unique=True)
|
om@3
|
880 |
|
om@3
|
881 |
return BaseTemplate.__call__(self, *a, **kw)
|
om@3
|
882 |
|
om@3
|
883 |
def generate_code(text, filename, parser=None):
|
om@3
|
884 |
# parse the text
|
om@3
|
885 |
parser = parser or Parser()
|
om@3
|
886 |
rootnode = parser.parse(text, filename)
|
om@3
|
887 |
|
om@3
|
888 |
# generate python code from the parse tree
|
om@3
|
889 |
code = rootnode.emit(indent="").strip()
|
om@3
|
890 |
return safestr(code)
|
om@3
|
891 |
|
om@3
|
892 |
generate_code = staticmethod(generate_code)
|
om@3
|
893 |
|
om@3
|
894 |
def create_parser(self):
|
om@3
|
895 |
p = Parser()
|
om@3
|
896 |
for ext in self.extensions:
|
om@3
|
897 |
p = ext(p)
|
om@3
|
898 |
return p
|
om@3
|
899 |
|
om@3
|
900 |
def compile_template(self, template_string, filename):
|
om@3
|
901 |
code = Template.generate_code(template_string, filename, parser=self.create_parser())
|
om@3
|
902 |
|
om@3
|
903 |
def get_source_line(filename, lineno):
|
om@3
|
904 |
try:
|
om@3
|
905 |
lines = open(filename).read().splitlines()
|
om@3
|
906 |
return lines[lineno]
|
om@3
|
907 |
except:
|
om@3
|
908 |
return None
|
om@3
|
909 |
|
om@3
|
910 |
try:
|
om@3
|
911 |
# compile the code first to report the errors, if any, with the filename
|
om@3
|
912 |
compiled_code = compile(code, filename, 'exec')
|
om@3
|
913 |
except SyntaxError, e:
|
om@3
|
914 |
# display template line that caused the error along with the traceback.
|
om@3
|
915 |
try:
|
om@3
|
916 |
e.msg += '\n\nTemplate traceback:\n File %s, line %s\n %s' % \
|
om@3
|
917 |
(repr(e.filename), e.lineno, get_source_line(e.filename, e.lineno-1))
|
om@3
|
918 |
except:
|
om@3
|
919 |
pass
|
om@3
|
920 |
raise
|
om@3
|
921 |
|
om@3
|
922 |
# make sure code is safe - but not with jython, it doesn't have a working compiler module
|
om@3
|
923 |
if not sys.platform.startswith('java'):
|
om@3
|
924 |
try:
|
om@3
|
925 |
import compiler
|
om@3
|
926 |
ast = compiler.parse(code)
|
om@3
|
927 |
SafeVisitor().walk(ast, filename)
|
om@3
|
928 |
except ImportError:
|
om@3
|
929 |
warnings.warn("Unabled to import compiler module. Unable to check templates for safety.")
|
om@3
|
930 |
else:
|
om@3
|
931 |
warnings.warn("SECURITY ISSUE: You are using Jython, which does not support checking templates for safety. Your templates can execute arbitrary code.")
|
om@3
|
932 |
|
om@3
|
933 |
return compiled_code
|
om@3
|
934 |
|
om@3
|
935 |
class CompiledTemplate(Template):
|
om@3
|
936 |
def __init__(self, f, filename):
|
om@3
|
937 |
Template.__init__(self, '', filename)
|
om@3
|
938 |
self.t = f
|
om@3
|
939 |
|
om@3
|
940 |
def compile_template(self, *a):
|
om@3
|
941 |
return None
|
om@3
|
942 |
|
om@3
|
943 |
def _compile(self, *a):
|
om@3
|
944 |
return None
|
om@3
|
945 |
|
om@3
|
946 |
class Render:
|
om@3
|
947 |
"""The most preferred way of using templates.
|
om@3
|
948 |
|
om@3
|
949 |
render = web.template.render('templates')
|
om@3
|
950 |
print render.foo()
|
om@3
|
951 |
|
om@3
|
952 |
Optional parameter can be `base` can be used to pass output of
|
om@3
|
953 |
every template through the base template.
|
om@3
|
954 |
|
om@3
|
955 |
render = web.template.render('templates', base='layout')
|
om@3
|
956 |
"""
|
om@3
|
957 |
def __init__(self, loc='templates', cache=None, base=None, **keywords):
|
om@3
|
958 |
self._loc = loc
|
om@3
|
959 |
self._keywords = keywords
|
om@3
|
960 |
|
om@3
|
961 |
if cache is None:
|
om@3
|
962 |
cache = not config.get('debug', False)
|
om@3
|
963 |
|
om@3
|
964 |
if cache:
|
om@3
|
965 |
self._cache = {}
|
om@3
|
966 |
else:
|
om@3
|
967 |
self._cache = None
|
om@3
|
968 |
|
om@3
|
969 |
if base and not hasattr(base, '__call__'):
|
om@3
|
970 |
# make base a function, so that it can be passed to sub-renders
|
om@3
|
971 |
self._base = lambda page: self._template(base)(page)
|
om@3
|
972 |
else:
|
om@3
|
973 |
self._base = base
|
om@3
|
974 |
|
om@3
|
975 |
def _add_global(self, obj, name=None):
|
om@3
|
976 |
"""Add a global to this rendering instance."""
|
om@3
|
977 |
if 'globals' not in self._keywords: self._keywords['globals'] = {}
|
om@3
|
978 |
if not name:
|
om@3
|
979 |
name = obj.__name__
|
om@3
|
980 |
self._keywords['globals'][name] = obj
|
om@3
|
981 |
|
om@3
|
982 |
def _lookup(self, name):
|
om@3
|
983 |
path = os.path.join(self._loc, name)
|
om@3
|
984 |
if os.path.isdir(path):
|
om@3
|
985 |
return 'dir', path
|
om@3
|
986 |
else:
|
om@3
|
987 |
path = self._findfile(path)
|
om@3
|
988 |
if path:
|
om@3
|
989 |
return 'file', path
|
om@3
|
990 |
else:
|
om@3
|
991 |
return 'none', None
|
om@3
|
992 |
|
om@3
|
993 |
def _load_template(self, name):
|
om@3
|
994 |
kind, path = self._lookup(name)
|
om@3
|
995 |
|
om@3
|
996 |
if kind == 'dir':
|
om@3
|
997 |
return Render(path, cache=self._cache is not None, base=self._base, **self._keywords)
|
om@3
|
998 |
elif kind == 'file':
|
om@3
|
999 |
return Template(open(path).read(), filename=path, **self._keywords)
|
om@3
|
1000 |
else:
|
om@3
|
1001 |
raise AttributeError, "No template named " + name
|
om@3
|
1002 |
|
om@3
|
1003 |
def _findfile(self, path_prefix):
|
om@3
|
1004 |
p = [f for f in glob.glob(path_prefix + '.*') if not f.endswith('~')] # skip backup files
|
om@3
|
1005 |
p.sort() # sort the matches for deterministic order
|
om@3
|
1006 |
return p and p[0]
|
om@3
|
1007 |
|
om@3
|
1008 |
def _template(self, name):
|
om@3
|
1009 |
if self._cache is not None:
|
om@3
|
1010 |
if name not in self._cache:
|
om@3
|
1011 |
self._cache[name] = self._load_template(name)
|
om@3
|
1012 |
return self._cache[name]
|
om@3
|
1013 |
else:
|
om@3
|
1014 |
return self._load_template(name)
|
om@3
|
1015 |
|
om@3
|
1016 |
def __getattr__(self, name):
|
om@3
|
1017 |
t = self._template(name)
|
om@3
|
1018 |
if self._base and isinstance(t, Template):
|
om@3
|
1019 |
def template(*a, **kw):
|
om@3
|
1020 |
return self._base(t(*a, **kw))
|
om@3
|
1021 |
return template
|
om@3
|
1022 |
else:
|
om@3
|
1023 |
return self._template(name)
|
om@3
|
1024 |
|
om@3
|
1025 |
class GAE_Render(Render):
|
om@3
|
1026 |
# Render gets over-written. make a copy here.
|
om@3
|
1027 |
super = Render
|
om@3
|
1028 |
def __init__(self, loc, *a, **kw):
|
om@3
|
1029 |
GAE_Render.super.__init__(self, loc, *a, **kw)
|
om@3
|
1030 |
|
om@3
|
1031 |
import types
|
om@3
|
1032 |
if isinstance(loc, types.ModuleType):
|
om@3
|
1033 |
self.mod = loc
|
om@3
|
1034 |
else:
|
om@3
|
1035 |
name = loc.rstrip('/').replace('/', '.')
|
om@3
|
1036 |
self.mod = __import__(name, None, None, ['x'])
|
om@3
|
1037 |
|
om@3
|
1038 |
self.mod.__dict__.update(kw.get('builtins', TEMPLATE_BUILTINS))
|
om@3
|
1039 |
self.mod.__dict__.update(Template.globals)
|
om@3
|
1040 |
self.mod.__dict__.update(kw.get('globals', {}))
|
om@3
|
1041 |
|
om@3
|
1042 |
def _load_template(self, name):
|
om@3
|
1043 |
t = getattr(self.mod, name)
|
om@3
|
1044 |
import types
|
om@3
|
1045 |
if isinstance(t, types.ModuleType):
|
om@3
|
1046 |
return GAE_Render(t, cache=self._cache is not None, base=self._base, **self._keywords)
|
om@3
|
1047 |
else:
|
om@3
|
1048 |
return t
|
om@3
|
1049 |
|
om@3
|
1050 |
render = Render
|
om@3
|
1051 |
# setup render for Google App Engine.
|
om@3
|
1052 |
try:
|
om@3
|
1053 |
from google import appengine
|
om@3
|
1054 |
render = Render = GAE_Render
|
om@3
|
1055 |
except ImportError:
|
om@3
|
1056 |
pass
|
om@3
|
1057 |
|
om@3
|
1058 |
def frender(path, **keywords):
|
om@3
|
1059 |
"""Creates a template from the given file path.
|
om@3
|
1060 |
"""
|
om@3
|
1061 |
return Template(open(path).read(), filename=path, **keywords)
|
om@3
|
1062 |
|
om@3
|
1063 |
def compile_templates(root):
|
om@3
|
1064 |
"""Compiles templates to python code."""
|
om@3
|
1065 |
re_start = re_compile('^', re.M)
|
om@3
|
1066 |
|
om@3
|
1067 |
for dirpath, dirnames, filenames in os.walk(root):
|
om@3
|
1068 |
filenames = [f for f in filenames if not f.startswith('.') and not f.endswith('~') and not f.startswith('__init__.py')]
|
om@3
|
1069 |
|
om@3
|
1070 |
for d in dirnames[:]:
|
om@3
|
1071 |
if d.startswith('.'):
|
om@3
|
1072 |
dirnames.remove(d) # don't visit this dir
|
om@3
|
1073 |
|
om@3
|
1074 |
out = open(os.path.join(dirpath, '__init__.py'), 'w')
|
om@3
|
1075 |
out.write('from web.template import CompiledTemplate, ForLoop, TemplateResult\n\n')
|
om@3
|
1076 |
if dirnames:
|
om@3
|
1077 |
out.write("import " + ", ".join(dirnames))
|
om@3
|
1078 |
out.write("\n")
|
om@3
|
1079 |
|
om@3
|
1080 |
for f in filenames:
|
om@3
|
1081 |
path = os.path.join(dirpath, f)
|
om@3
|
1082 |
|
om@3
|
1083 |
if '.' in f:
|
om@3
|
1084 |
name, _ = f.split('.', 1)
|
om@3
|
1085 |
else:
|
om@3
|
1086 |
name = f
|
om@3
|
1087 |
|
om@3
|
1088 |
text = open(path).read()
|
om@3
|
1089 |
text = Template.normalize_text(text)
|
om@3
|
1090 |
code = Template.generate_code(text, path)
|
om@3
|
1091 |
|
om@3
|
1092 |
code = code.replace("__template__", name, 1)
|
om@3
|
1093 |
|
om@3
|
1094 |
out.write(code)
|
om@3
|
1095 |
|
om@3
|
1096 |
out.write('\n\n')
|
om@3
|
1097 |
out.write('%s = CompiledTemplate(%s, %s)\n' % (name, name, repr(path)))
|
om@3
|
1098 |
out.write("join_ = %s._join; escape_ = %s._escape\n\n" % (name, name))
|
om@3
|
1099 |
|
om@3
|
1100 |
# create template to make sure it compiles
|
om@3
|
1101 |
t = Template(open(path).read(), path)
|
om@3
|
1102 |
out.close()
|
om@3
|
1103 |
|
om@3
|
1104 |
class ParseError(Exception):
|
om@3
|
1105 |
pass
|
om@3
|
1106 |
|
om@3
|
1107 |
class SecurityError(Exception):
|
om@3
|
1108 |
"""The template seems to be trying to do something naughty."""
|
om@3
|
1109 |
pass
|
om@3
|
1110 |
|
om@3
|
1111 |
# Enumerate all the allowed AST nodes
|
om@3
|
1112 |
ALLOWED_AST_NODES = [
|
om@3
|
1113 |
"Add", "And",
|
om@3
|
1114 |
# "AssAttr",
|
om@3
|
1115 |
"AssList", "AssName", "AssTuple",
|
om@3
|
1116 |
# "Assert",
|
om@3
|
1117 |
"Assign", "AugAssign",
|
om@3
|
1118 |
# "Backquote",
|
om@3
|
1119 |
"Bitand", "Bitor", "Bitxor", "Break",
|
om@3
|
1120 |
"CallFunc","Class", "Compare", "Const", "Continue",
|
om@3
|
1121 |
"Decorators", "Dict", "Discard", "Div",
|
om@3
|
1122 |
"Ellipsis", "EmptyNode",
|
om@3
|
1123 |
# "Exec",
|
om@3
|
1124 |
"Expression", "FloorDiv", "For",
|
om@3
|
1125 |
# "From",
|
om@3
|
1126 |
"Function",
|
om@3
|
1127 |
"GenExpr", "GenExprFor", "GenExprIf", "GenExprInner",
|
om@3
|
1128 |
"Getattr",
|
om@3
|
1129 |
# "Global",
|
om@3
|
1130 |
"If", "IfExp",
|
om@3
|
1131 |
# "Import",
|
om@3
|
1132 |
"Invert", "Keyword", "Lambda", "LeftShift",
|
om@3
|
1133 |
"List", "ListComp", "ListCompFor", "ListCompIf", "Mod",
|
om@3
|
1134 |
"Module",
|
om@3
|
1135 |
"Mul", "Name", "Not", "Or", "Pass", "Power",
|
om@3
|
1136 |
# "Print", "Printnl", "Raise",
|
om@3
|
1137 |
"Return", "RightShift", "Slice", "Sliceobj",
|
om@3
|
1138 |
"Stmt", "Sub", "Subscript",
|
om@3
|
1139 |
# "TryExcept", "TryFinally",
|
om@3
|
1140 |
"Tuple", "UnaryAdd", "UnarySub",
|
om@3
|
1141 |
"While", "With", "Yield",
|
om@3
|
1142 |
]
|
om@3
|
1143 |
|
om@3
|
1144 |
class SafeVisitor(object):
|
om@3
|
1145 |
"""
|
om@3
|
1146 |
Make sure code is safe by walking through the AST.
|
om@3
|
1147 |
|
om@3
|
1148 |
Code considered unsafe if:
|
om@3
|
1149 |
* it has restricted AST nodes
|
om@3
|
1150 |
* it is trying to access resricted attributes
|
om@3
|
1151 |
|
om@3
|
1152 |
Adopted from http://www.zafar.se/bkz/uploads/safe.txt (public domain, Babar K. Zafar)
|
om@3
|
1153 |
"""
|
om@3
|
1154 |
def __init__(self):
|
om@3
|
1155 |
"Initialize visitor by generating callbacks for all AST node types."
|
om@3
|
1156 |
self.errors = []
|
om@3
|
1157 |
|
om@3
|
1158 |
def walk(self, ast, filename):
|
om@3
|
1159 |
"Validate each node in AST and raise SecurityError if the code is not safe."
|
om@3
|
1160 |
self.filename = filename
|
om@3
|
1161 |
self.visit(ast)
|
om@3
|
1162 |
|
om@3
|
1163 |
if self.errors:
|
om@3
|
1164 |
raise SecurityError, '\n'.join([str(err) for err in self.errors])
|
om@3
|
1165 |
|
om@3
|
1166 |
def visit(self, node, *args):
|
om@3
|
1167 |
"Recursively validate node and all of its children."
|
om@3
|
1168 |
def classname(obj):
|
om@3
|
1169 |
return obj.__class__.__name__
|
om@3
|
1170 |
nodename = classname(node)
|
om@3
|
1171 |
fn = getattr(self, 'visit' + nodename, None)
|
om@3
|
1172 |
|
om@3
|
1173 |
if fn:
|
om@3
|
1174 |
fn(node, *args)
|
om@3
|
1175 |
else:
|
om@3
|
1176 |
if nodename not in ALLOWED_AST_NODES:
|
om@3
|
1177 |
self.fail(node, *args)
|
om@3
|
1178 |
|
om@3
|
1179 |
for child in node.getChildNodes():
|
om@3
|
1180 |
self.visit(child, *args)
|
om@3
|
1181 |
|
om@3
|
1182 |
def visitName(self, node, *args):
|
om@3
|
1183 |
"Disallow any attempts to access a restricted attr."
|
om@3
|
1184 |
#self.assert_attr(node.getChildren()[0], node)
|
om@3
|
1185 |
pass
|
om@3
|
1186 |
|
om@3
|
1187 |
def visitGetattr(self, node, *args):
|
om@3
|
1188 |
"Disallow any attempts to access a restricted attribute."
|
om@3
|
1189 |
self.assert_attr(node.attrname, node)
|
om@3
|
1190 |
|
om@3
|
1191 |
def assert_attr(self, attrname, node):
|
om@3
|
1192 |
if self.is_unallowed_attr(attrname):
|
om@3
|
1193 |
lineno = self.get_node_lineno(node)
|
om@3
|
1194 |
e = SecurityError("%s:%d - access to attribute '%s' is denied" % (self.filename, lineno, attrname))
|
om@3
|
1195 |
self.errors.append(e)
|
om@3
|
1196 |
|
om@3
|
1197 |
def is_unallowed_attr(self, name):
|
om@3
|
1198 |
return name.startswith('_') \
|
om@3
|
1199 |
or name.startswith('func_') \
|
om@3
|
1200 |
or name.startswith('im_')
|
om@3
|
1201 |
|
om@3
|
1202 |
def get_node_lineno(self, node):
|
om@3
|
1203 |
return (node.lineno) and node.lineno or 0
|
om@3
|
1204 |
|
om@3
|
1205 |
def fail(self, node, *args):
|
om@3
|
1206 |
"Default callback for unallowed AST nodes."
|
om@3
|
1207 |
lineno = self.get_node_lineno(node)
|
om@3
|
1208 |
nodename = node.__class__.__name__
|
om@3
|
1209 |
e = SecurityError("%s:%d - execution of '%s' statements is denied" % (self.filename, lineno, nodename))
|
om@3
|
1210 |
self.errors.append(e)
|
om@3
|
1211 |
|
om@3
|
1212 |
class TemplateResult(object, DictMixin):
|
om@3
|
1213 |
"""Dictionary like object for storing template output.
|
om@3
|
1214 |
|
om@3
|
1215 |
The result of a template execution is usally a string, but sometimes it
|
om@3
|
1216 |
contains attributes set using $var. This class provides a simple
|
om@3
|
1217 |
dictionary like interface for storing the output of the template and the
|
om@3
|
1218 |
attributes. The output is stored with a special key __body__. Convering
|
om@3
|
1219 |
the the TemplateResult to string or unicode returns the value of __body__.
|
om@3
|
1220 |
|
om@3
|
1221 |
When the template is in execution, the output is generated part by part
|
om@3
|
1222 |
and those parts are combined at the end. Parts are added to the
|
om@3
|
1223 |
TemplateResult by calling the `extend` method and the parts are combined
|
om@3
|
1224 |
seemlessly when __body__ is accessed.
|
om@3
|
1225 |
|
om@3
|
1226 |
>>> d = TemplateResult(__body__='hello, world', x='foo')
|
om@3
|
1227 |
>>> d
|
om@3
|
1228 |
<TemplateResult: {'__body__': 'hello, world', 'x': 'foo'}>
|
om@3
|
1229 |
>>> print d
|
om@3
|
1230 |
hello, world
|
om@3
|
1231 |
>>> d.x
|
om@3
|
1232 |
'foo'
|
om@3
|
1233 |
>>> d = TemplateResult()
|
om@3
|
1234 |
>>> d.extend([u'hello', u'world'])
|
om@3
|
1235 |
>>> d
|
om@3
|
1236 |
<TemplateResult: {'__body__': u'helloworld'}>
|
om@3
|
1237 |
"""
|
om@3
|
1238 |
def __init__(self, *a, **kw):
|
om@3
|
1239 |
self.__dict__["_d"] = dict(*a, **kw)
|
om@3
|
1240 |
self._d.setdefault("__body__", u'')
|
om@3
|
1241 |
|
om@3
|
1242 |
self.__dict__['_parts'] = []
|
om@3
|
1243 |
self.__dict__["extend"] = self._parts.extend
|
om@3
|
1244 |
|
om@3
|
1245 |
self._d.setdefault("__body__", None)
|
om@3
|
1246 |
|
om@3
|
1247 |
def keys(self):
|
om@3
|
1248 |
return self._d.keys()
|
om@3
|
1249 |
|
om@3
|
1250 |
def _prepare_body(self):
|
om@3
|
1251 |
"""Prepare value of __body__ by joining parts.
|
om@3
|
1252 |
"""
|
om@3
|
1253 |
if self._parts:
|
om@3
|
1254 |
value = u"".join(self._parts)
|
om@3
|
1255 |
self._parts[:] = []
|
om@3
|
1256 |
body = self._d.get('__body__')
|
om@3
|
1257 |
if body:
|
om@3
|
1258 |
self._d['__body__'] = body + value
|
om@3
|
1259 |
else:
|
om@3
|
1260 |
self._d['__body__'] = value
|
om@3
|
1261 |
|
om@3
|
1262 |
def __getitem__(self, name):
|
om@3
|
1263 |
if name == "__body__":
|
om@3
|
1264 |
self._prepare_body()
|
om@3
|
1265 |
return self._d[name]
|
om@3
|
1266 |
|
om@3
|
1267 |
def __setitem__(self, name, value):
|
om@3
|
1268 |
if name == "__body__":
|
om@3
|
1269 |
self._prepare_body()
|
om@3
|
1270 |
return self._d.__setitem__(name, value)
|
om@3
|
1271 |
|
om@3
|
1272 |
def __delitem__(self, name):
|
om@3
|
1273 |
if name == "__body__":
|
om@3
|
1274 |
self._prepare_body()
|
om@3
|
1275 |
return self._d.__delitem__(name)
|
om@3
|
1276 |
|
om@3
|
1277 |
def __getattr__(self, key):
|
om@3
|
1278 |
try:
|
om@3
|
1279 |
return self[key]
|
om@3
|
1280 |
except KeyError, k:
|
om@3
|
1281 |
raise AttributeError, k
|
om@3
|
1282 |
|
om@3
|
1283 |
def __setattr__(self, key, value):
|
om@3
|
1284 |
self[key] = value
|
om@3
|
1285 |
|
om@3
|
1286 |
def __delattr__(self, key):
|
om@3
|
1287 |
try:
|
om@3
|
1288 |
del self[key]
|
om@3
|
1289 |
except KeyError, k:
|
om@3
|
1290 |
raise AttributeError, k
|
om@3
|
1291 |
|
om@3
|
1292 |
def __unicode__(self):
|
om@3
|
1293 |
self._prepare_body()
|
om@3
|
1294 |
return self["__body__"]
|
om@3
|
1295 |
|
om@3
|
1296 |
def __str__(self):
|
om@3
|
1297 |
self._prepare_body()
|
om@3
|
1298 |
return self["__body__"].encode('utf-8')
|
om@3
|
1299 |
|
om@3
|
1300 |
def __repr__(self):
|
om@3
|
1301 |
self._prepare_body()
|
om@3
|
1302 |
return "<TemplateResult: %s>" % self._d
|
om@3
|
1303 |
|
om@3
|
1304 |
def test():
|
om@3
|
1305 |
r"""Doctest for testing template module.
|
om@3
|
1306 |
|
om@3
|
1307 |
Define a utility function to run template test.
|
om@3
|
1308 |
|
om@3
|
1309 |
>>> class TestResult:
|
om@3
|
1310 |
... def __init__(self, t): self.t = t
|
om@3
|
1311 |
... def __getattr__(self, name): return getattr(self.t, name)
|
om@3
|
1312 |
... def __repr__(self): return repr(unicode(self))
|
om@3
|
1313 |
...
|
om@3
|
1314 |
>>> def t(code, **keywords):
|
om@3
|
1315 |
... tmpl = Template(code, **keywords)
|
om@3
|
1316 |
... return lambda *a, **kw: TestResult(tmpl(*a, **kw))
|
om@3
|
1317 |
...
|
om@3
|
1318 |
|
om@3
|
1319 |
Simple tests.
|
om@3
|
1320 |
|
om@3
|
1321 |
>>> t('1')()
|
om@3
|
1322 |
u'1\n'
|
om@3
|
1323 |
>>> t('$def with ()\n1')()
|
om@3
|
1324 |
u'1\n'
|
om@3
|
1325 |
>>> t('$def with (a)\n$a')(1)
|
om@3
|
1326 |
u'1\n'
|
om@3
|
1327 |
>>> t('$def with (a=0)\n$a')(1)
|
om@3
|
1328 |
u'1\n'
|
om@3
|
1329 |
>>> t('$def with (a=0)\n$a')(a=1)
|
om@3
|
1330 |
u'1\n'
|
om@3
|
1331 |
|
om@3
|
1332 |
Test complicated expressions.
|
om@3
|
1333 |
|
om@3
|
1334 |
>>> t('$def with (x)\n$x.upper()')('hello')
|
om@3
|
1335 |
u'HELLO\n'
|
om@3
|
1336 |
>>> t('$(2 * 3 + 4 * 5)')()
|
om@3
|
1337 |
u'26\n'
|
om@3
|
1338 |
>>> t('${2 * 3 + 4 * 5}')()
|
om@3
|
1339 |
u'26\n'
|
om@3
|
1340 |
>>> t('$def with (limit)\nkeep $(limit)ing.')('go')
|
om@3
|
1341 |
u'keep going.\n'
|
om@3
|
1342 |
>>> t('$def with (a)\n$a.b[0]')(storage(b=[1]))
|
om@3
|
1343 |
u'1\n'
|
om@3
|
1344 |
|
om@3
|
1345 |
Test html escaping.
|
om@3
|
1346 |
|
om@3
|
1347 |
>>> t('$def with (x)\n$x', filename='a.html')('<html>')
|
om@3
|
1348 |
u'<html>\n'
|
om@3
|
1349 |
>>> t('$def with (x)\n$x', filename='a.txt')('<html>')
|
om@3
|
1350 |
u'<html>\n'
|
om@3
|
1351 |
|
om@3
|
1352 |
Test if, for and while.
|
om@3
|
1353 |
|
om@3
|
1354 |
>>> t('$if 1: 1')()
|
om@3
|
1355 |
u'1\n'
|
om@3
|
1356 |
>>> t('$if 1:\n 1')()
|
om@3
|
1357 |
u'1\n'
|
om@3
|
1358 |
>>> t('$if 1:\n 1\\')()
|
om@3
|
1359 |
u'1'
|
om@3
|
1360 |
>>> t('$if 0: 0\n$elif 1: 1')()
|
om@3
|
1361 |
u'1\n'
|
om@3
|
1362 |
>>> t('$if 0: 0\n$elif None: 0\n$else: 1')()
|
om@3
|
1363 |
u'1\n'
|
om@3
|
1364 |
>>> t('$if 0 < 1 and 1 < 2: 1')()
|
om@3
|
1365 |
u'1\n'
|
om@3
|
1366 |
>>> t('$for x in [1, 2, 3]: $x')()
|
om@3
|
1367 |
u'1\n2\n3\n'
|
om@3
|
1368 |
>>> t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1})
|
om@3
|
1369 |
u'1\n'
|
om@3
|
1370 |
>>> t('$for x in [1, 2, 3]:\n\t$x')()
|
om@3
|
1371 |
u' 1\n 2\n 3\n'
|
om@3
|
1372 |
>>> t('$def with (a)\n$while a and a.pop():1')([1, 2, 3])
|
om@3
|
1373 |
u'1\n1\n1\n'
|
om@3
|
1374 |
|
om@3
|
1375 |
The space after : must be ignored.
|
om@3
|
1376 |
|
om@3
|
1377 |
>>> t('$if True: foo')()
|
om@3
|
1378 |
u'foo\n'
|
om@3
|
1379 |
|
om@3
|
1380 |
Test loop.xxx.
|
om@3
|
1381 |
|
om@3
|
1382 |
>>> t("$for i in range(5):$loop.index, $loop.parity")()
|
om@3
|
1383 |
u'1, odd\n2, even\n3, odd\n4, even\n5, odd\n'
|
om@3
|
1384 |
>>> t("$for i in range(2):\n $for j in range(2):$loop.parent.parity $loop.parity")()
|
om@3
|
1385 |
u'odd odd\nodd even\neven odd\neven even\n'
|
om@3
|
1386 |
|
om@3
|
1387 |
Test assignment.
|
om@3
|
1388 |
|
om@3
|
1389 |
>>> t('$ a = 1\n$a')()
|
om@3
|
1390 |
u'1\n'
|
om@3
|
1391 |
>>> t('$ a = [1]\n$a[0]')()
|
om@3
|
1392 |
u'1\n'
|
om@3
|
1393 |
>>> t('$ a = {1: 1}\n$a.keys()[0]')()
|
om@3
|
1394 |
u'1\n'
|
om@3
|
1395 |
>>> t('$ a = []\n$if not a: 1')()
|
om@3
|
1396 |
u'1\n'
|
om@3
|
1397 |
>>> t('$ a = {}\n$if not a: 1')()
|
om@3
|
1398 |
u'1\n'
|
om@3
|
1399 |
>>> t('$ a = -1\n$a')()
|
om@3
|
1400 |
u'-1\n'
|
om@3
|
1401 |
>>> t('$ a = "1"\n$a')()
|
om@3
|
1402 |
u'1\n'
|
om@3
|
1403 |
|
om@3
|
1404 |
Test comments.
|
om@3
|
1405 |
|
om@3
|
1406 |
>>> t('$# 0')()
|
om@3
|
1407 |
u'\n'
|
om@3
|
1408 |
>>> t('hello$#comment1\nhello$#comment2')()
|
om@3
|
1409 |
u'hello\nhello\n'
|
om@3
|
1410 |
>>> t('$#comment0\nhello$#comment1\nhello$#comment2')()
|
om@3
|
1411 |
u'\nhello\nhello\n'
|
om@3
|
1412 |
|
om@3
|
1413 |
Test unicode.
|
om@3
|
1414 |
|
om@3
|
1415 |
>>> t('$def with (a)\n$a')(u'\u203d')
|
om@3
|
1416 |
u'\u203d\n'
|
om@3
|
1417 |
>>> t('$def with (a)\n$a')(u'\u203d'.encode('utf-8'))
|
om@3
|
1418 |
u'\u203d\n'
|
om@3
|
1419 |
>>> t(u'$def with (a)\n$a $:a')(u'\u203d')
|
om@3
|
1420 |
u'\u203d \u203d\n'
|
om@3
|
1421 |
>>> t(u'$def with ()\nfoo')()
|
om@3
|
1422 |
u'foo\n'
|
om@3
|
1423 |
>>> def f(x): return x
|
om@3
|
1424 |
...
|
om@3
|
1425 |
>>> t(u'$def with (f)\n$:f("x")')(f)
|
om@3
|
1426 |
u'x\n'
|
om@3
|
1427 |
>>> t('$def with (f)\n$:f("x")')(f)
|
om@3
|
1428 |
u'x\n'
|
om@3
|
1429 |
|
om@3
|
1430 |
Test dollar escaping.
|
om@3
|
1431 |
|
om@3
|
1432 |
>>> t("Stop, $$money isn't evaluated.")()
|
om@3
|
1433 |
u"Stop, $money isn't evaluated.\n"
|
om@3
|
1434 |
>>> t("Stop, \$money isn't evaluated.")()
|
om@3
|
1435 |
u"Stop, $money isn't evaluated.\n"
|
om@3
|
1436 |
|
om@3
|
1437 |
Test space sensitivity.
|
om@3
|
1438 |
|
om@3
|
1439 |
>>> t('$def with (x)\n$x')(1)
|
om@3
|
1440 |
u'1\n'
|
om@3
|
1441 |
>>> t('$def with(x ,y)\n$x')(1, 1)
|
om@3
|
1442 |
u'1\n'
|
om@3
|
1443 |
>>> t('$(1 + 2*3 + 4)')()
|
om@3
|
1444 |
u'11\n'
|
om@3
|
1445 |
|
om@3
|
1446 |
Make sure globals are working.
|
om@3
|
1447 |
|
om@3
|
1448 |
>>> t('$x')()
|
om@3
|
1449 |
Traceback (most recent call last):
|
om@3
|
1450 |
...
|
om@3
|
1451 |
NameError: global name 'x' is not defined
|
om@3
|
1452 |
>>> t('$x', globals={'x': 1})()
|
om@3
|
1453 |
u'1\n'
|
om@3
|
1454 |
|
om@3
|
1455 |
Can't change globals.
|
om@3
|
1456 |
|
om@3
|
1457 |
>>> t('$ x = 2\n$x', globals={'x': 1})()
|
om@3
|
1458 |
u'2\n'
|
om@3
|
1459 |
>>> t('$ x = x + 1\n$x', globals={'x': 1})()
|
om@3
|
1460 |
Traceback (most recent call last):
|
om@3
|
1461 |
...
|
om@3
|
1462 |
UnboundLocalError: local variable 'x' referenced before assignment
|
om@3
|
1463 |
|
om@3
|
1464 |
Make sure builtins are customizable.
|
om@3
|
1465 |
|
om@3
|
1466 |
>>> t('$min(1, 2)')()
|
om@3
|
1467 |
u'1\n'
|
om@3
|
1468 |
>>> t('$min(1, 2)', builtins={})()
|
om@3
|
1469 |
Traceback (most recent call last):
|
om@3
|
1470 |
...
|
om@3
|
1471 |
NameError: global name 'min' is not defined
|
om@3
|
1472 |
|
om@3
|
1473 |
Test vars.
|
om@3
|
1474 |
|
om@3
|
1475 |
>>> x = t('$var x: 1')()
|
om@3
|
1476 |
>>> x.x
|
om@3
|
1477 |
u'1'
|
om@3
|
1478 |
>>> x = t('$var x = 1')()
|
om@3
|
1479 |
>>> x.x
|
om@3
|
1480 |
1
|
om@3
|
1481 |
>>> x = t('$var x: \n foo\n bar')()
|
om@3
|
1482 |
>>> x.x
|
om@3
|
1483 |
u'foo\nbar\n'
|
om@3
|
1484 |
|
om@3
|
1485 |
Test BOM chars.
|
om@3
|
1486 |
|
om@3
|
1487 |
>>> t('\xef\xbb\xbf$def with(x)\n$x')('foo')
|
om@3
|
1488 |
u'foo\n'
|
om@3
|
1489 |
|
om@3
|
1490 |
Test for with weird cases.
|
om@3
|
1491 |
|
om@3
|
1492 |
>>> t('$for i in range(10)[1:5]:\n $i')()
|
om@3
|
1493 |
u'1\n2\n3\n4\n'
|
om@3
|
1494 |
>>> t("$for k, v in {'a': 1, 'b': 2}.items():\n $k $v")()
|
om@3
|
1495 |
u'a 1\nb 2\n'
|
om@3
|
1496 |
>>> t("$for k, v in ({'a': 1, 'b': 2}.items():\n $k $v")()
|
om@3
|
1497 |
Traceback (most recent call last):
|
om@3
|
1498 |
...
|
om@3
|
1499 |
SyntaxError: invalid syntax
|
om@3
|
1500 |
|
om@3
|
1501 |
Test datetime.
|
om@3
|
1502 |
|
om@3
|
1503 |
>>> import datetime
|
om@3
|
1504 |
>>> t("$def with (date)\n$date.strftime('%m %Y')")(datetime.datetime(2009, 1, 1))
|
om@3
|
1505 |
u'01 2009\n'
|
om@3
|
1506 |
"""
|
om@3
|
1507 |
pass
|
om@3
|
1508 |
|
om@3
|
1509 |
if __name__ == "__main__":
|
om@3
|
1510 |
import sys
|
om@3
|
1511 |
if '--compile' in sys.argv:
|
om@3
|
1512 |
compile_templates(sys.argv[2])
|
om@3
|
1513 |
else:
|
om@3
|
1514 |
import doctest
|
om@3
|
1515 |
doctest.testmod()
|