Mercurial > genshi > mirror
comparison markup/eval.py @ 87:1b874f032bde trunk
Fix some problems in expression evaluation by transforming the AST and compiling that to bytecode, instead of generating bytecode directly. Invalidates #13.
author | cmlenz |
---|---|
date | Mon, 17 Jul 2006 15:08:53 +0000 |
parents | a54ebae77330 |
children | 628ba9ed39ef |
comparison
equal
deleted
inserted
replaced
86:a54ebae77330 | 87:1b874f032bde |
---|---|
11 # individuals. For the exact contribution history, see the revision | 11 # individuals. For the exact contribution history, see the revision |
12 # history and logs, available at http://markup.edgewall.org/log/. | 12 # history and logs, available at http://markup.edgewall.org/log/. |
13 | 13 |
14 """Support for "safe" evaluation of Python expressions.""" | 14 """Support for "safe" evaluation of Python expressions.""" |
15 | 15 |
16 from __future__ import division | |
17 | |
18 import __builtin__ | 16 import __builtin__ |
19 from compiler import parse, pycodegen | 17 from compiler import ast, parse |
18 from compiler.pycodegen import ExpressionCodeGenerator | |
20 | 19 |
21 from markup.core import Stream | 20 from markup.core import Stream |
22 | 21 |
23 __all__ = ['Expression'] | 22 __all__ = ['Expression'] |
24 | 23 |
63 >>> data = dict(items=[1, 2, 3]) | 62 >>> data = dict(items=[1, 2, 3]) |
64 >>> Expression('len(items)').evaluate(data) | 63 >>> Expression('len(items)').evaluate(data) |
65 3 | 64 3 |
66 """ | 65 """ |
67 __slots__ = ['source', 'code'] | 66 __slots__ = ['source', 'code'] |
68 _visitors = {} | |
69 | 67 |
70 def __init__(self, source, filename=None, lineno=-1): | 68 def __init__(self, source, filename=None, lineno=-1): |
71 """Create the expression. | 69 """Create the expression. |
72 | 70 |
73 @param source: the expression as string | 71 @param source: the expression as string |
74 """ | 72 """ |
75 self.source = source | 73 self.source = source |
76 | 74 self.code = self._compile(source, filename, lineno) |
77 ast = parse(self.source, 'eval') | |
78 if isinstance(filename, unicode): | |
79 # pycodegen doesn't like unicode in the filename | |
80 filename = filename.encode('utf-8', 'replace') | |
81 ast.filename = filename or '<string>' | |
82 gen = TemplateExpressionCodeGenerator(ast) | |
83 if lineno >= 0: | |
84 gen.emit('SET_LINENO', lineno) | |
85 self.code = gen.getCode() | |
86 | 75 |
87 def __repr__(self): | 76 def __repr__(self): |
88 return '<Expression "%s">' % self.source | 77 return '<Expression "%s">' % self.source |
89 | 78 |
90 def evaluate(self, data): | 79 def evaluate(self, data): |
93 @param data: a mapping containing the data to evaluate against | 82 @param data: a mapping containing the data to evaluate against |
94 @return: the result of the evaluation | 83 @return: the result of the evaluation |
95 """ | 84 """ |
96 return eval(self.code) | 85 return eval(self.code) |
97 | 86 |
98 | 87 def _compile(self, source, filename, lineno): |
99 class TemplateExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator): | 88 tree = parse(self.source, 'eval') |
89 xform = ExpressionASTTransformer() | |
90 tree = xform.visit(tree) | |
91 | |
92 if isinstance(filename, unicode): | |
93 # pycodegen doesn't like unicode in the filename | |
94 filename = filename.encode('utf-8', 'replace') | |
95 tree.filename = filename or '<string>' | |
96 | |
97 gen = ExpressionCodeGenerator(tree) | |
98 if lineno >= 0: | |
99 gen.emit('SET_LINENO', lineno) | |
100 | |
101 return gen.getCode() | |
102 | |
103 def _lookup_name(self, data, name): | |
104 val = data.get(name) | |
105 if val is None: | |
106 val = getattr(__builtin__, name, None) | |
107 return val | |
108 | |
109 def _lookup_attribute(self, data, obj, key): | |
110 if hasattr(obj, key): | |
111 return getattr(obj, key) | |
112 try: | |
113 return obj[key] | |
114 except (KeyError, TypeError): | |
115 return None | |
116 | |
117 def _lookup_item(self, data, obj, key): | |
118 if len(key) == 1: | |
119 key = key[0] | |
120 try: | |
121 return obj[key] | |
122 except (KeyError, IndexError, TypeError), e: | |
123 pass | |
124 if isinstance(key, basestring): | |
125 try: | |
126 return getattr(obj, key) | |
127 except (AttributeError, TypeError), e: | |
128 pass | |
129 | |
130 | |
131 class ASTTransformer(object): | |
132 """General purpose base class for AST transformations. | |
133 | |
134 Every visitor method can be overridden to return an AST node that has been | |
135 altered or replaced in some way. | |
136 """ | |
137 _visitors = {} | |
138 | |
139 def visit(self, node): | |
140 v = self._visitors.get(node.__class__) | |
141 if not v: | |
142 v = getattr(self, 'visit%s' % node.__class__.__name__) | |
143 self._visitors[node.__class__] = v | |
144 return v(node) | |
145 | |
146 def visitExpression(self, node): | |
147 node.node = self.visit(node.node) | |
148 return node | |
149 | |
150 # Functions & Accessors | |
151 | |
152 def visitCallFunc(self, node): | |
153 node.node = self.visit(node.node) | |
154 node.args = map(self.visit, node.args) | |
155 if node.star_args: | |
156 node.star_args = map(self.visit, node.star_args) | |
157 if node.dstar_args: | |
158 node.dstart_args = map(self.visit, node.dstar_args) | |
159 return node | |
100 | 160 |
101 def visitGetattr(self, node): | 161 def visitGetattr(self, node): |
102 """Overridden to fallback to item access if the object doesn't have an | 162 node.expr = self.visit(node.expr) |
103 attribute. | 163 return node |
104 | 164 |
105 Also, if either method fails, this returns `None` instead of raising an | 165 def visitSubscript(self, node): |
106 `AttributeError`. | 166 node.expr = self.visit(node.expr) |
107 """ | 167 node.subs = map(self.visit, node.subs) |
108 # check whether the object has the request attribute | 168 return node |
109 self.visit(node.expr) | 169 |
110 self.emit('STORE_NAME', 'obj') | 170 # Operators |
111 self.emit('LOAD_GLOBAL', 'hasattr') | 171 |
112 self.emit('LOAD_NAME', 'obj') | 172 def _visitBoolOp(self, node): |
113 self.emit('LOAD_CONST', node.attrname) | 173 node.nodes = map(self.visit, node.nodes) |
114 self.emit('CALL_FUNCTION', 2) | 174 return node |
115 else_ = self.newBlock() | 175 visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp |
116 self.emit('JUMP_IF_FALSE', else_) | 176 |
117 self.emit('POP_TOP') | 177 def _visitBinOp(self, node): |
118 | 178 node.left = self.visit(node.left) |
119 # hasattr returned True, so return the attribute value | 179 node.right = self.visit(node.right) |
120 self.emit('LOAD_NAME', 'obj') | 180 return node |
121 self.emit('LOAD_ATTR', node.attrname) | 181 visitAdd = visitSub = _visitBinOp |
122 self.emit('STORE_NAME', 'val') | 182 visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp |
123 return_ = self.newBlock() | 183 visitLeftShift = visitRightShift = _visitBinOp |
124 self.emit('JUMP_FORWARD', return_) | 184 |
125 | 185 def visitCompare(self, node): |
126 # hasattr returned False, so try item access | 186 node.expr = self.visit(node.expr) |
127 self.startBlock(else_) | 187 node.ops = map(lambda (op, expr): (op, self.visit(expr)), |
128 try_ = self.newBlock() | 188 node.ops) |
129 except_ = self.newBlock() | 189 return node |
130 self.emit('SETUP_EXCEPT', except_) | 190 |
131 self.nextBlock(try_) | 191 def _visitUnaryOp(self, node): |
132 self.setups.push((pycodegen.EXCEPT, try_)) | 192 node.expr = self.visit(node.expr) |
133 self.emit('LOAD_NAME', 'obj') | 193 return node |
134 self.emit('LOAD_CONST', node.attrname) | 194 visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp |
135 self.emit('BINARY_SUBSCR') | 195 |
136 self.emit('STORE_NAME', 'val') | 196 # Identifiers & Literals |
137 self.emit('POP_BLOCK') | 197 |
138 self.setups.pop() | 198 def _visitDefault(self, node): |
139 self.emit('JUMP_FORWARD', return_) | 199 return node |
140 | 200 visitConst = visitKeyword = visitName = _visitDefault |
141 # exception handler: just return `None` | 201 |
142 self.startBlock(except_) | 202 def visitDict(self, node): |
143 self.emit('DUP_TOP') | 203 node.items = map(lambda (k, v): (self.visit(k), self.visit(v)), |
144 self.emit('LOAD_GLOBAL', 'KeyError') | 204 node.items) |
145 self.emit('LOAD_GLOBAL', 'TypeError') | 205 return node |
146 self.emit('BUILD_TUPLE', 2) | 206 |
147 self.emit('COMPARE_OP', 'exception match') | 207 def visitTuple(self, node): |
148 next = self.newBlock() | 208 node.nodes = map(lambda n: self.visit(n), node.nodes) |
149 self.emit('JUMP_IF_FALSE', next) | 209 return node |
150 self.nextBlock() | 210 |
151 self.emit('POP_TOP') | 211 def visitList(self, node): |
152 self.emit('POP_TOP') | 212 node.nodes = map(lambda n: self.visit(n), node.nodes) |
153 self.emit('POP_TOP') | 213 return node |
154 self.emit('POP_TOP') | 214 |
155 self.emit('LOAD_CONST', None) # exception handler body | 215 |
156 self.emit('STORE_NAME', 'val') | 216 class ExpressionASTTransformer(ASTTransformer): |
157 self.emit('JUMP_FORWARD', return_) | 217 """Concrete AST transformer that implementations the AST transformations |
158 self.nextBlock(next) | 218 needed for template expressions. |
159 self.emit('POP_TOP') | 219 """ |
160 self.emit('END_FINALLY') | 220 |
161 | 221 def visitGetattr(self, node): |
162 # return | 222 return ast.CallFunc( |
163 self.nextBlock(return_) | 223 ast.Getattr(ast.Name('self'), '_lookup_attribute'), |
164 self.emit('LOAD_NAME', 'val') | 224 [ast.Name('data'), self.visit(node.expr), ast.Const(node.attrname)] |
225 ) | |
165 | 226 |
166 def visitName(self, node): | 227 def visitName(self, node): |
167 """Overridden to lookup names in the context data instead of in | 228 return ast.CallFunc( |
168 locals/globals. | 229 ast.Getattr(ast.Name('self'), '_lookup_name'), |
169 | 230 [ast.Name('data'), ast.Const(node.name)] |
170 If a name is not found in the context data, we fall back to Python | 231 ) |
171 builtins. | 232 return node |
172 """ | 233 |
173 next = self.newBlock() | 234 def visitSubscript(self, node): |
174 end = self.newBlock() | 235 return ast.CallFunc( |
175 | 236 ast.Getattr(ast.Name('self'), '_lookup_item'), |
176 # default: lookup in context data | 237 [ast.Name('data'), self.visit(node.expr), |
177 self.loadName('data') | 238 ast.Tuple(map(self.visit, node.subs))] |
178 self.emit('LOAD_ATTR', 'get') | 239 ) |
179 self.emit('LOAD_CONST', node.name) | |
180 self.emit('CALL_FUNCTION', 1) | |
181 self.emit('STORE_NAME', 'val') | |
182 | |
183 # test whether the value "is None" | |
184 self.emit('LOAD_NAME', 'val') | |
185 self.emit('LOAD_CONST', None) | |
186 self.emit('COMPARE_OP', 'is') | |
187 self.emit('JUMP_IF_FALSE', next) | |
188 self.emit('POP_TOP') | |
189 | |
190 # if it is, fallback to builtins | |
191 self.emit('LOAD_GLOBAL', 'getattr') | |
192 self.emit('LOAD_GLOBAL', '__builtin__') | |
193 self.emit('LOAD_CONST', node.name) | |
194 self.emit('LOAD_CONST', None) | |
195 self.emit('CALL_FUNCTION', 3) | |
196 self.emit('STORE_NAME', 'val') | |
197 self.emit('JUMP_FORWARD', end) | |
198 | |
199 self.nextBlock(next) | |
200 self.emit('POP_TOP') | |
201 | |
202 self.nextBlock(end) | |
203 self.emit('LOAD_NAME', 'val') | |
204 | |
205 def visitSubscript(self, node, aug_flag=None): | |
206 """Overridden to fallback to attribute access if the object doesn't | |
207 have an item (or doesn't even support item access). | |
208 | |
209 If either method fails, this returns `None` instead of raising an | |
210 `IndexError`, `KeyError`, or `TypeError`. | |
211 """ | |
212 self.visit(node.expr) | |
213 self.emit('STORE_NAME', 'obj') | |
214 | |
215 if len(node.subs) > 1: | |
216 # For non-scalar subscripts, use the default method | |
217 # FIXME: this should catch exceptions | |
218 self.emit('LOAD_NAME', 'obj') | |
219 for sub in node.subs: | |
220 self.visit(sub) | |
221 self.emit('BUILD_TUPLE', len(node.subs)) | |
222 self.emit('BINARY_SUBSCR') | |
223 | |
224 else: | |
225 # For a scalar subscript, fallback to attribute access | |
226 # FIXME: Would be nice if we could limit this to string subscripts | |
227 try_ = self.newBlock() | |
228 except_ = self.newBlock() | |
229 return_ = self.newBlock() | |
230 self.emit('SETUP_EXCEPT', except_) | |
231 self.nextBlock(try_) | |
232 self.setups.push((pycodegen.EXCEPT, try_)) | |
233 self.emit('LOAD_NAME', 'obj') | |
234 self.visit(node.subs[0]) | |
235 self.emit('BINARY_SUBSCR') | |
236 self.emit('STORE_NAME', 'val') | |
237 self.emit('POP_BLOCK') | |
238 self.setups.pop() | |
239 self.emit('JUMP_FORWARD', return_) | |
240 | |
241 self.startBlock(except_) | |
242 self.emit('DUP_TOP') | |
243 self.emit('LOAD_GLOBAL', 'KeyError') | |
244 self.emit('LOAD_GLOBAL', 'IndexError') | |
245 self.emit('LOAD_GLOBAL', 'TypeError') | |
246 self.emit('BUILD_TUPLE', 3) | |
247 self.emit('COMPARE_OP', 'exception match') | |
248 next = self.newBlock() | |
249 self.emit('JUMP_IF_FALSE', next) | |
250 self.nextBlock() | |
251 self.emit('POP_TOP') | |
252 self.emit('POP_TOP') | |
253 self.emit('POP_TOP') | |
254 self.emit('POP_TOP') | |
255 self.emit('LOAD_GLOBAL', 'getattr') # exception handler body | |
256 self.emit('LOAD_NAME', 'obj') | |
257 self.visit(node.subs[0]) | |
258 self.emit('LOAD_CONST', None) | |
259 self.emit('CALL_FUNCTION', 3) | |
260 self.emit('STORE_NAME', 'val') | |
261 self.emit('JUMP_FORWARD', return_) | |
262 self.nextBlock(next) | |
263 self.emit('POP_TOP') | |
264 self.emit('END_FINALLY') | |
265 | |
266 # return | |
267 self.nextBlock(return_) | |
268 self.emit('LOAD_NAME', 'val') |