Mercurial > genshi > mirror
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]) |