comparison markup/eval.py @ 87:c6f07b7cd3ea

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 5d98c4259d68
children 9ecae580dd93
comparison
equal deleted inserted replaced
86:5d98c4259d68 87:c6f07b7cd3ea
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')
Copyright (C) 2012-2017 Edgewall Software