comparison markup/eval.py @ 81:d60486018004 trunk

Template expressions are now compiled to Python bytecode.
author cmlenz
date Sat, 15 Jul 2006 11:29:25 +0000
parents c40a5dcd2b55
children 5ca4be55ad0b
comparison
equal deleted inserted replaced
80:e0957965553f 81:d60486018004
14 """Support for "safe" evaluation of Python expressions.""" 14 """Support for "safe" evaluation of Python expressions."""
15 15
16 from __future__ import division 16 from __future__ import division
17 17
18 import __builtin__ 18 import __builtin__
19 try: 19 from compiler import parse, pycodegen
20 import _ast # Python 2.5
21 except ImportError:
22 _ast = None
23 import compiler
24 import operator
25 20
26 from markup.core import Stream 21 from markup.core import Stream
27 22
28 __all__ = ['Expression'] 23 __all__ = ['Expression']
29 24
32 """Evaluates Python expressions used in templates. 27 """Evaluates Python expressions used in templates.
33 28
34 >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) 29 >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
35 >>> Expression('test').evaluate(data) 30 >>> Expression('test').evaluate(data)
36 'Foo' 31 'Foo'
32
37 >>> Expression('items[0]').evaluate(data) 33 >>> Expression('items[0]').evaluate(data)
38 1 34 1
39 >>> Expression('items[-1]').evaluate(data) 35 >>> Expression('items[-1]').evaluate(data)
40 3 36 3
41 >>> Expression('dict["some"]').evaluate(data) 37 >>> Expression('dict["some"]').evaluate(data)
44 Similar to e.g. Javascript, expressions in templates can use the dot 40 Similar to e.g. Javascript, expressions in templates can use the dot
45 notation for attribute access to access items in mappings: 41 notation for attribute access to access items in mappings:
46 42
47 >>> Expression('dict.some').evaluate(data) 43 >>> Expression('dict.some').evaluate(data)
48 'thing' 44 'thing'
49 45 """
46 """
50 This also works the other way around: item access can be used to access 47 This also works the other way around: item access can be used to access
51 any object attribute (meaning there's no use for `getattr()` in templates): 48 any object attribute (meaning there's no use for `getattr()` in templates):
52 49
53 >>> class MyClass(object): 50 >>> class MyClass(object):
54 ... myattr = 'Bar' 51 ... myattr = 'Bar'
66 63
67 >>> data = dict(items=[1, 2, 3]) 64 >>> data = dict(items=[1, 2, 3])
68 >>> Expression('len(items)').evaluate(data) 65 >>> Expression('len(items)').evaluate(data)
69 3 66 3
70 """ 67 """
71 __slots__ = ['source', 'ast'] 68 __slots__ = ['source', 'code']
72 _visitors = {} 69 _visitors = {}
73 70
74 def __init__(self, source): 71 def __init__(self, source, filename=None, lineno=-1):
75 """Create the expression. 72 """Create the expression.
76 73
77 @param source: the expression as string 74 @param source: the expression as string
78 """ 75 """
79 self.source = source 76 self.source = source
80 self.ast = None 77
78 tree = parse(self.source, 'eval')
79 if isinstance(filename, unicode):
80 # pycodegen doesn't like unicode in the filename
81 filename = filename.encode('utf-8', 'replace')
82 tree.filename = filename or '<string>'
83 gen = TemplateExpressionCodeGenerator(tree)
84 if lineno >= 0:
85 gen.emit('SET_LINENO', lineno)
86 self.code = gen.getCode()
81 87
82 def __repr__(self): 88 def __repr__(self):
83 return '<Expression "%s">' % self.source 89 return '<Expression "%s">' % self.source
84 90
85 if _ast is None: 91 def evaluate(self, data):
86 92 """Evaluate the expression against the given data dictionary.
87 def evaluate(self, data): 93
88 """Evaluate the expression against the given data dictionary. 94 @param data: a mapping containing the data to evaluate against
89 95 @return: the result of the evaluation
90 @param data: a mapping containing the data to evaluate against 96 """
91 @return: the result of the evaluation 97 return eval(self.code)
92 """ 98
93 if not self.ast: 99
94 self.ast = compiler.parse(self.source, 'eval') 100 class TemplateExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator):
95 return self._visit(self.ast.node, data) 101
96 102 def visitGetattr(self, node):
97 # AST traversal 103 """Overridden to fallback to item access if the object doesn't have an
98 104 attribute.
99 def _visit(self, node, data): 105
100 v = self._visitors.get(node.__class__) 106 Also, if either method fails, this returns `None` instead of raising an
101 if not v: 107 `AttributeError`.
102 v = getattr(self, '_visit_%s' % node.__class__.__name__.lower()) 108 """
103 self._visitors[node.__class__] = v 109 # check whether the object has the request attribute
104 return v(node, data) 110 self.visit(node.expr)
105 111 self.emit('STORE_NAME', 'obj')
106 def _visit_expression(self, node, data): 112 self.emit('LOAD_GLOBAL', 'hasattr')
107 for child in node.getChildNodes(): 113 self.emit('LOAD_NAME', 'obj')
108 return self._visit(child, data) 114 self.emit('LOAD_CONST', node.attrname)
109 115 self.emit('CALL_FUNCTION', 2)
110 # Functions & Accessors 116 else_ = self.newBlock()
111 117 self.emit('JUMP_IF_FALSE', else_)
112 def _visit_callfunc(self, node, data): 118 self.emit('POP_TOP')
113 func = self._visit(node.node, data) 119
114 if func is None: 120 # hasattr returned True, so return the attribute value
115 return None 121 self.emit('LOAD_NAME', 'obj')
116 args = [self._visit(arg, data) for arg in node.args 122 self.emit('LOAD_ATTR', node.attrname)
117 if not isinstance(arg, compiler.ast.Keyword)] 123 self.emit('STORE_NAME', 'val')
118 kwargs = dict([(arg.name, self._visit(arg.expr, data)) for arg 124 return_ = self.newBlock()
119 in node.args if isinstance(arg, compiler.ast.Keyword)]) 125 self.emit('JUMP_FORWARD', return_)
120 return func(*args, **kwargs) 126
121 127 # hasattr returned False, so try item access
122 def _visit_getattr(self, node, data): 128 self.startBlock(else_)
123 obj = self._visit(node.expr, data) 129 try_ = self.newBlock()
124 if hasattr(obj, node.attrname): 130 except_ = self.newBlock()
125 return getattr(obj, node.attrname) 131 self.emit('SETUP_EXCEPT', except_)
126 try: 132 self.nextBlock(try_)
127 return obj[node.attrname] 133 self.setups.push((pycodegen.EXCEPT, try_))
128 except (KeyError, TypeError): 134 self.emit('LOAD_NAME', 'obj')
129 return None 135 self.emit('LOAD_CONST', node.attrname)
130 136 self.emit('BINARY_SUBSCR')
131 def _visit_slice(self, node, data): 137 self.emit('STORE_NAME', 'val')
132 obj = self._visit(node.expr, data) 138 self.emit('POP_BLOCK')
133 lower = node.lower and self._visit(node.lower, data) or None 139 self.setups.pop()
134 upper = node.upper and self._visit(node.upper, data) or None 140 self.emit('JUMP_FORWARD', return_)
135 return obj[lower:upper] 141
136 142 # exception handler: just return `None`
137 def _visit_subscript(self, node, data): 143 self.startBlock(except_)
138 obj = self._visit(node.expr, data) 144 self.emit('DUP_TOP')
139 subs = map(lambda sub: self._visit(sub, data), node.subs) 145 self.emit('LOAD_GLOBAL', 'KeyError')
140 if len(subs) == 1: 146 self.emit('LOAD_GLOBAL', 'TypeError')
141 subs = subs[0] 147 self.emit('BUILD_TUPLE', 2)
142 try: 148 self.emit('COMPARE_OP', 'exception match')
143 return obj[subs] 149 next = self.newBlock()
144 except (KeyError, IndexError, TypeError): 150 self.emit('JUMP_IF_FALSE', next)
145 try: 151 self.nextBlock()
146 return getattr(obj, subs) 152 self.emit('POP_TOP')
147 except (AttributeError, TypeError): 153 self.emit('POP_TOP')
148 return None 154 self.emit('POP_TOP')
149 155 self.emit('POP_TOP')
150 # Operators 156 self.emit('LOAD_CONST', None) # exception handler body
151 157 self.emit('STORE_NAME', 'val')
152 def _visit_and(self, node, data): 158 self.emit('JUMP_FORWARD', return_)
153 return reduce(lambda x, y: x and y, 159 self.nextBlock(next)
154 [self._visit(n, data) for n in node.nodes]) 160 self.emit('POP_TOP')
155 161 self.emit('END_FINALLY')
156 def _visit_or(self, node, data): 162
157 return reduce(lambda x, y: x or y, 163 # return
158 [self._visit(n, data) for n in node.nodes]) 164 self.nextBlock(return_)
159 165 self.emit('LOAD_NAME', 'val')
160 def _visit_bitand(self, node, data): 166
161 return reduce(operator.and_, 167 def visitName(self, node):
162 [self._visit(n, data) for n in node.nodes]) 168 """Overridden to lookup names in the context data instead of in
163 169 locals/globals.
164 def _visit_bitor(self, node, data): 170
165 return reduce(operator.or_, 171 If a name is not found in the context data, we fall back to Python
166 [self._visit(n, data) for n in node.nodes]) 172 builtins.
167 173 """
168 _OP_MAP = {'==': operator.eq, '!=': operator.ne, 174 next = self.newBlock()
169 '<': operator.lt, '<=': operator.le, 175 end = self.newBlock()
170 '>': operator.gt, '>=': operator.ge, 176
171 'is': operator.is_, 'is not': operator.is_not, 177 # default: lookup in context data
172 'in': lambda x, y: operator.contains(y, x), 178 self.loadName('data')
173 'not in': lambda x, y: not operator.contains(y, x)} 179 self.emit('LOAD_ATTR', 'get')
174 def _visit_compare(self, node, data): 180 self.emit('LOAD_CONST', node.name)
175 result = self._visit(node.expr, data) 181 self.emit('CALL_FUNCTION', 1)
176 ops = node.ops[:] 182 self.emit('STORE_NAME', 'val')
177 ops.reverse() 183
178 for op, rval in ops: 184 # test whether the value "is None"
179 result = self._OP_MAP[op](result, self._visit(rval, data)) 185 self.emit('LOAD_NAME', 'val')
180 return result 186 self.emit('LOAD_CONST', None)
181 187 self.emit('COMPARE_OP', 'is')
182 def _visit_add(self, node, data): 188 self.emit('JUMP_IF_FALSE', next)
183 return self._visit(node.left, data) + self._visit(node.right, data) 189 self.emit('POP_TOP')
184 190
185 def _visit_div(self, node, data): 191 # if it is, fallback to builtins
186 return self._visit(node.left, data) / self._visit(node.right, data) 192 self.emit('LOAD_GLOBAL', 'getattr')
187 193 self.emit('LOAD_GLOBAL', '__builtin__')
188 def _visit_floordiv(self, node, data): 194 self.emit('LOAD_CONST', node.name)
189 return self._visit(node.left, data) // self._visit(node.right, data) 195 self.emit('LOAD_CONST', None)
190 196 self.emit('CALL_FUNCTION', 3)
191 def _visit_mod(self, node, data): 197 self.emit('STORE_NAME', 'val')
192 return self._visit(node.left, data) % self._visit(node.right, data) 198 self.emit('JUMP_FORWARD', end)
193 199
194 def _visit_mul(self, node, data): 200 self.nextBlock(next)
195 return self._visit(node.left, data) * self._visit(node.right, data) 201 self.emit('POP_TOP')
196 202
197 def _visit_power(self, node, data): 203 self.nextBlock(end)
198 return self._visit(node.left, data) ** self._visit(node.right, data) 204 self.emit('LOAD_NAME', 'val')
199 205
200 def _visit_sub(self, node, data): 206 def visitSubscript(self, node, aug_flag=None):
201 return self._visit(node.left, data) - self._visit(node.right, data) 207 """Overridden to fallback to attribute access if the object doesn't
202 208 have an item (or doesn't even support item access).
203 def _visit_unaryadd(self, node, data): 209
204 return +self._visit(node.expr, data) 210 If either method fails, this returns `None` instead of raising an
205 211 `IndexError`, `KeyError`, or `TypeError`.
206 def _visit_unarysub(self, node, data): 212 """
207 return -self._visit(node.expr, data) 213 self.visit(node.expr)
208 214 self.emit('STORE_NAME', 'obj')
209 def _visit_not(self, node, data): 215
210 return not self._visit(node.expr, data) 216 if len(node.subs) > 1:
211 217 # For non-scalar subscripts, use the default method
212 def _visit_invert(self, node, data): 218 # FIXME: this should catch exceptions
213 return ~self._visit(node.expr, data) 219 self.emit('LOAD_NAME', 'obj')
214 220 for sub in node.subs:
215 # Identifiers & Literals 221 self.visit(sub)
216 222 self.emit('BUILD_TUPLE', len(node.subs))
217 def _visit_name(self, node, data): 223 self.emit('BINARY_SUBSCR')
218 val = data.get(node.name) 224
219 if val is None: 225 else:
220 val = getattr(__builtin__, node.name, None) 226 # For a scalar subscript, fallback to attribute access
221 return val 227 # FIXME: Would be nice if we could limit this to string subscripts
222 228 try_ = self.newBlock()
223 def _visit_const(self, node, data): 229 except_ = self.newBlock()
224 return node.value 230 return_ = self.newBlock()
225 231 self.emit('SETUP_EXCEPT', except_)
226 def _visit_dict(self, node, data): 232 self.nextBlock(try_)
227 return dict([(self._visit(k, data), self._visit(v, data)) 233 self.setups.push((pycodegen.EXCEPT, try_))
228 for k, v in node.items]) 234 self.emit('LOAD_NAME', 'obj')
229 235 self.visit(node.subs[0])
230 def _visit_tuple(self, node, data): 236 self.emit('BINARY_SUBSCR')
231 return tuple([self._visit(n, data) for n in node.nodes]) 237 self.emit('STORE_NAME', 'val')
232 238 self.emit('POP_BLOCK')
233 def _visit_list(self, node, data): 239 self.setups.pop()
234 return [self._visit(n, data) for n in node.nodes] 240 self.emit('JUMP_FORWARD', return_)
235 241
236 else: 242 self.startBlock(except_)
237 243 self.emit('DUP_TOP')
238 def evaluate(self, data): 244 self.emit('LOAD_GLOBAL', 'KeyError')
239 """Evaluate the expression against the given data dictionary. 245 self.emit('LOAD_GLOBAL', 'IndexError')
240 246 self.emit('LOAD_GLOBAL', 'TypeError')
241 @param data: a mapping containing the data to evaluate against 247 self.emit('BUILD_TUPLE', 3)
242 @return: the result of the evaluation 248 self.emit('COMPARE_OP', 'exception match')
243 """ 249 next = self.newBlock()
244 if not self.ast: 250 self.emit('JUMP_IF_FALSE', next)
245 self.ast = compile(self.source, '?', 'eval', 0x400) 251 self.nextBlock()
246 return self._visit(self.ast, data) 252 self.emit('POP_TOP')
247 253 self.emit('POP_TOP')
248 # AST traversal 254 self.emit('POP_TOP')
249 255 self.emit('POP_TOP')
250 def _visit(self, node, data): 256 self.emit('LOAD_GLOBAL', 'getattr') # exception handler body
251 v = self._visitors.get(node.__class__) 257 self.emit('LOAD_NAME', 'obj')
252 if not v: 258 self.visit(node.subs[0])
253 v = getattr(self, '_visit_%s' % node.__class__.__name__.lower()) 259 self.emit('LOAD_CONST', None)
254 self._visitors[node.__class__] = v 260 self.emit('CALL_FUNCTION', 3)
255 return v(node, data) 261 self.emit('STORE_NAME', 'val')
256 262 self.emit('JUMP_FORWARD', return_)
257 def _visit_expression(self, node, data): 263 self.nextBlock(next)
258 return self._visit(node.body, data) 264 self.emit('POP_TOP')
259 265 self.emit('END_FINALLY')
260 # Functions & Accessors 266
261 267 # return
262 def _visit_attribute(self, node, data): 268 self.nextBlock(return_)
263 obj = self._visit(node.value, data) 269 self.emit('LOAD_NAME', 'val')
264 if hasattr(obj, node.attr): 270
265 return getattr(obj, node.attr) 271
266 try: 272 if __name__ == '__main__':
267 return obj[node.attr] 273 import doctest
268 except (KeyError, TypeError): 274 doctest.testmod()
269 return None
270
271 def _visit_call(self, node, data):
272 func = self._visit(node.func, data)
273 if func is None:
274 return None
275 args = [self._visit(arg, data) for arg in node.args]
276 kwargs = dict([(kwarg.arg, self._visit(kwarg.value, data))
277 for kwarg in node.keywords])
278 return func(*args, **kwargs)
279
280 def _visit_subscript(self, node, data):
281 obj = self._visit(node.value, data)
282 if isinstance(node.slice, _ast.Slice):
283 try:
284 return obj[self._visit(lower, data):
285 self._visit(upper, data):
286 self._visit(step, data)]
287 except (KeyError, IndexError, TypeError):
288 pass
289 else:
290 index = self._visit(node.slice.value, data)
291 try:
292 return obj[index]
293 except (KeyError, IndexError, TypeError):
294 try:
295 return getattr(obj, index)
296 except (AttributeError, TypeError):
297 pass
298 return None
299
300 # Operators
301
302 _OP_MAP = {_ast.Add: operator.add, _ast.And: lambda l, r: l and r,
303 _ast.BitAnd: operator.and_, _ast.BitOr: operator.or_,
304 _ast.Div: operator.truediv, _ast.Eq: operator.eq,
305 _ast.FloorDiv: operator.floordiv, _ast.Gt: operator.gt,
306 _ast.GtE: operator.ge, _ast.Invert: operator.inv,
307 _ast.In: lambda l, r: operator.contains(r, l),
308 _ast.Is: operator.is_, _ast.IsNot: operator.is_not,
309 _ast.Lt: operator.lt, _ast.LtE: operator.le,
310 _ast.Mod: operator.mod, _ast.Mult: operator.mul,
311 _ast.Not: operator.not_, _ast.NotEq: operator.ne,
312 _ast.NotIn: lambda l, r: not operator.contains(r, l),
313 _ast.Or: lambda l, r: l or r, _ast.Pow: operator.pow,
314 _ast.Sub: operator.sub, _ast.UAdd: operator.pos,
315 _ast.USub: operator.neg}
316
317 def _visit_unaryop(self, node, data):
318 return self._OP_MAP[node.op.__class__](self._visit(node.operand, data))
319
320 def _visit_binop(self, node, data):
321 return self._OP_MAP[node.op.__class__](self._visit(node.left, data),
322 self._visit(node.right, data))
323
324 def _visit_boolop(self, node, data):
325 return reduce(self._OP_MAP[node.op.__class__],
326 [self._visit(n, data) for n in node.values])
327
328 def _visit_compare(self, node, data):
329 result = self._visit(node.left, data)
330 ops = node.ops[:]
331 ops.reverse()
332 for op, rval in zip(ops, node.comparators):
333 result = self._OP_MAP[op.__class__](result,
334 self._visit(rval, data))
335 return result
336
337 # Identifiers & Literals
338
339 def _visit_dict(self, node, data):
340 return dict([(self._visit(k, data), self._visit(v, data))
341 for k, v in zip(node.keys, node.values)])
342
343 def _visit_list(self, node, data):
344 return [self._visit(n, data) for n in node.elts]
345
346 def _visit_name(self, node, data):
347 val = data.get(node.id)
348 if val is None:
349 val = getattr(__builtin__, node.id, None)
350 return val
351
352 def _visit_num(self, node, data):
353 return node.n
354
355 def _visit_str(self, node, data):
356 return node.s
357
358 def _visit_tuple(self, node, data):
359 return tuple([self._visit(n, data) for n in node.elts])
Copyright (C) 2012-2017 Edgewall Software