Edgewall Software

source: branches/stable/0.3.x/genshi/eval.py

Last change on this file was 470, checked in by cmlenz, 17 years ago

0.3.x branch: add unit test for Undefined checks in template expressions.

  • Property svn:eol-style set to native
File size: 14.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Support for "safe" evaluation of Python expressions."""
15
16import __builtin__
17from compiler import ast, parse
18from compiler.pycodegen import ExpressionCodeGenerator
19import new
20
21__all__ = ['Expression', 'Undefined']
22
23
24class Expression(object):
25    """Evaluates Python expressions used in templates.
26
27    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
28    >>> Expression('test').evaluate(data)
29    'Foo'
30
31    >>> Expression('items[0]').evaluate(data)
32    1
33    >>> Expression('items[-1]').evaluate(data)
34    3
35    >>> Expression('dict["some"]').evaluate(data)
36    'thing'
37   
38    Similar to e.g. Javascript, expressions in templates can use the dot
39    notation for attribute access to access items in mappings:
40   
41    >>> Expression('dict.some').evaluate(data)
42    'thing'
43   
44    This also works the other way around: item access can be used to access
45    any object attribute (meaning there's no use for `getattr()` in templates):
46   
47    >>> class MyClass(object):
48    ...     myattr = 'Bar'
49    >>> data = dict(mine=MyClass(), key='myattr')
50    >>> Expression('mine.myattr').evaluate(data)
51    'Bar'
52    >>> Expression('mine["myattr"]').evaluate(data)
53    'Bar'
54    >>> Expression('mine[key]').evaluate(data)
55    'Bar'
56   
57    All of the standard Python operators are available to template expressions.
58    Built-in functions such as `len()` are also available in template
59    expressions:
60   
61    >>> data = dict(items=[1, 2, 3])
62    >>> Expression('len(items)').evaluate(data)
63    3
64   
65    Undefined variables can be accessed in expressions, and evaluate to a
66    non-truth value for tests. To check whether a particular variable is
67    defined, its type can be compared to the special builtin class `Undefined`:
68   
69    >>> Expression('foo').evaluate({})
70    undefined
71    >>> Expression('type(foo) is Undefined').evaluate({})
72    True
73    """
74    __slots__ = ['source', 'code']
75
76    def __init__(self, source, filename=None, lineno=-1):
77        if isinstance(source, basestring):
78            self.source = source
79            self.code = _compile(_parse(source), self.source, filename=filename,
80                                 lineno=lineno)
81        else:
82            assert isinstance(source, ast.Node)
83            self.source = '?'
84            self.code = _compile(ast.Expression(source), filename=filename,
85                                 lineno=lineno)
86
87    def __repr__(self):
88        return '<Expression "%s">' % self.source
89
90    def evaluate(self, data, nocall=False):
91        """Evaluate the expression against the given data dictionary.
92       
93        @param data: a mapping containing the data to evaluate against
94        @param nocall: if true, the result of the evaluation is not called if
95            if it is a callable
96        @return: the result of the evaluation
97        """
98        retval = eval(self.code, {'data': data,
99                                  '_lookup_name': _lookup_name,
100                                  '_lookup_attr': _lookup_attr,
101                                  '_lookup_item': _lookup_item},
102                                 {'data': data})
103        if not nocall and type(retval) is not Undefined and callable(retval):
104            retval = retval()
105        return retval
106
107
108class Undefined(object):
109    """Represents a reference to an undefined variable.
110   
111    Unlike the Python runtime, template expressions can refer to an undefined
112    variable without causing a `NameError` to be raised. The result will be an
113    instance of the `Undefined´ class, which is treated the same as `False` in
114    conditions, and acts as an empty collection in iterations:
115   
116    >>> foo = Undefined('foo')
117    >>> bool(foo)
118    False
119    >>> list(foo)
120    []
121    >>> print foo
122    undefined
123   
124    However, calling an undefined variable, or trying to access an attribute
125    of that variable, will raise an exception that includes the name used to
126    reference that undefined variable.
127   
128    >>> foo('bar')
129    Traceback (most recent call last):
130        ...
131    NameError: Variable "foo" is not defined
132
133    >>> foo.bar
134    Traceback (most recent call last):
135        ...
136    NameError: Variable "foo" is not defined
137    """
138    __slots__ = ['name']
139
140    def __init__(self, name):
141        self.name = name
142
143    def __call__(self, *args, **kwargs):
144        self.throw()
145
146    def __getattr__(self, name):
147        self.throw()
148
149    def __iter__(self):
150        return iter([])
151
152    def __nonzero__(self):
153        return False
154
155    def __repr__(self):
156        return 'undefined'
157
158    def throw(self):
159        raise NameError('Variable "%s" is not defined' % self.name)
160
161
162def _parse(source, mode='eval'):
163    if isinstance(source, unicode):
164        source = '\xef\xbb\xbf' + source.encode('utf-8')
165    return parse(source, mode)
166
167def _compile(node, source=None, filename=None, lineno=-1):
168    tree = ExpressionASTTransformer().visit(node)
169    if isinstance(filename, unicode):
170        # unicode file names not allowed for code objects
171        filename = filename.encode('utf-8', 'replace')
172    elif not filename:
173        filename = '<string>'
174    tree.filename = filename
175    if lineno <= 0:
176        lineno = 1
177
178    gen = ExpressionCodeGenerator(tree)
179    gen.optimized = True
180    code = gen.getCode()
181
182    # We'd like to just set co_firstlineno, but it's readonly. So we need to
183    # clone the code object while adjusting the line number
184    return new.code(0, code.co_nlocals, code.co_stacksize,
185                    code.co_flags | 0x0040, code.co_code, code.co_consts,
186                    code.co_names, code.co_varnames, filename,
187                    '<Expression %s>' % (repr(source).replace("'", '"') or '?'),
188                    lineno, code.co_lnotab, (), ())
189
190BUILTINS = __builtin__.__dict__.copy()
191BUILTINS['Undefined'] = Undefined
192
193def _lookup_name(data, name, locals_=None):
194    val = Undefined
195    if locals_:
196        val = locals_.get(name, val)
197    if val is Undefined:
198        val = data.get(name, val)
199        if val is Undefined:
200            val = BUILTINS.get(name, val)
201            if val is not Undefined or name == 'Undefined':
202                return val
203        else:
204            return val
205    else:
206        return val
207    return val(name)
208
209def _lookup_attr(data, obj, key):
210    if type(obj) is Undefined:
211        obj.throw()
212    if hasattr(obj, key):
213        return getattr(obj, key)
214    try:
215        return obj[key]
216    except (KeyError, TypeError):
217        return None
218
219def _lookup_item(data, obj, key):
220    if type(obj) is Undefined:
221        obj.throw()
222    if len(key) == 1:
223        key = key[0]
224    try:
225        return obj[key]
226    except (KeyError, IndexError, TypeError), e:
227        if isinstance(key, basestring):
228            try:
229                return getattr(obj, key)
230            except (AttributeError, TypeError), e:
231                pass
232
233
234class ASTTransformer(object):
235    """General purpose base class for AST transformations.
236   
237    Every visitor method can be overridden to return an AST node that has been
238    altered or replaced in some way.
239    """
240    _visitors = {}
241
242    def visit(self, node, *args, **kwargs):
243        v = self._visitors.get(node.__class__)
244        if not v:
245            v = getattr(self, 'visit%s' % node.__class__.__name__)
246            self._visitors[node.__class__] = v
247        return v(node, *args, **kwargs)
248
249    def visitExpression(self, node, *args, **kwargs):
250        node.node = self.visit(node.node, *args, **kwargs)
251        return node
252
253    # Functions & Accessors
254
255    def visitCallFunc(self, node, *args, **kwargs):
256        node.node = self.visit(node.node, *args, **kwargs)
257        node.args = [self.visit(x, *args, **kwargs) for x in node.args]
258        if node.star_args:
259            node.star_args = self.visit(node.star_args, *args, **kwargs)
260        if node.dstar_args:
261            node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
262        return node
263
264    def visitLambda(self, node, *args, **kwargs):
265        node.code = self.visit(node.code, *args, **kwargs)
266        node.filename = '<string>' # workaround for bug in pycodegen
267        return node
268
269    def visitGetattr(self, node, *args, **kwargs):
270        node.expr = self.visit(node.expr, *args, **kwargs)
271        return node
272
273    def visitSubscript(self, node, *args, **kwargs):
274        node.expr = self.visit(node.expr, *args, **kwargs)
275        node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
276        return node
277
278    # Operators
279
280    def _visitBoolOp(self, node, *args, **kwargs):
281        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
282        return node
283    visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
284
285    def _visitBinOp(self, node, *args, **kwargs):
286        node.left = self.visit(node.left, *args, **kwargs)
287        node.right = self.visit(node.right, *args, **kwargs)
288        return node
289    visitAdd = visitSub = _visitBinOp
290    visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
291    visitLeftShift = visitRightShift = _visitBinOp
292
293    def visitCompare(self, node, *args, **kwargs):
294        node.expr = self.visit(node.expr, *args, **kwargs)
295        node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in  node.ops]
296        return node
297
298    def _visitUnaryOp(self, node, *args, **kwargs):
299        node.expr = self.visit(node.expr, *args, **kwargs)
300        return node
301    visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
302    visitBackquote = _visitUnaryOp
303
304    # Identifiers, Literals and Comprehensions
305
306    def _visitDefault(self, node, *args, **kwargs):
307        return node
308    visitAssName = visitAssTuple = _visitDefault
309    visitConst = visitName = _visitDefault
310
311    def visitDict(self, node, *args, **kwargs):
312        node.items = [(self.visit(k, *args, **kwargs),
313                       self.visit(v, *args, **kwargs)) for k, v in node.items]
314        return node
315
316    def visitGenExpr(self, node, *args, **kwargs):
317        node.code = self.visit(node.code, *args, **kwargs)
318        node.filename = '<string>' # workaround for bug in pycodegen
319        return node
320
321    def visitGenExprFor(self, node, *args, **kwargs):
322        node.assign = self.visit(node.assign, *args, **kwargs)
323        node.iter = self.visit(node.iter, *args, **kwargs)
324        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
325        return node
326
327    def visitGenExprIf(self, node, *args, **kwargs):
328        node.test = self.visit(node.test, *args, **kwargs)
329        return node
330
331    def visitGenExprInner(self, node, *args, **kwargs):
332        node.expr = self.visit(node.expr, *args, **kwargs)
333        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
334        return node
335
336    def visitKeyword(self, node, *args, **kwargs):
337        node.expr = self.visit(node.expr, *args, **kwargs)
338        return node
339
340    def visitList(self, node, *args, **kwargs):
341        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
342        return node
343
344    def visitListComp(self, node, *args, **kwargs):
345        node.expr = self.visit(node.expr, *args, **kwargs)
346        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
347        return node
348
349    def visitListCompFor(self, node, *args, **kwargs):
350        node.assign = self.visit(node.assign, *args, **kwargs)
351        node.list = self.visit(node.list, *args, **kwargs)
352        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
353        return node
354
355    def visitListCompIf(self, node, *args, **kwargs):
356        node.test = self.visit(node.test, *args, **kwargs)
357        return node
358
359    def visitSlice(self, node, *args, **kwargs):
360        node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
361        if node.lower is not None:
362            node.lower = self.visit(node.lower, *args, **kwargs)
363        if node.upper is not None:
364            node.upper = self.visit(node.upper, *args, **kwargs)
365        return node
366
367    def visitSliceobj(self, node, *args, **kwargs):
368        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
369        return node
370
371    def visitTuple(self, node, *args, **kwargs):
372        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
373        return node
374
375
376class ExpressionASTTransformer(ASTTransformer):
377    """Concrete AST transformer that implements the AST transformations needed
378    for template expressions.
379    """
380
381    def visitConst(self, node, locals_=False):
382        if isinstance(node.value, str):
383            return ast.Const(node.value.decode('utf-8'))
384        return node
385
386    def visitGenExprIf(self, node, *args, **kwargs):
387        node.test = self.visit(node.test, locals_=True)
388        return node
389
390    def visitGenExprInner(self, node, *args, **kwargs):
391        node.expr = self.visit(node.expr, locals_=True)
392        node.quals = [self.visit(x) for x in node.quals]
393        return node
394
395    def visitGetattr(self, node, locals_=False):
396        return ast.CallFunc(ast.Name('_lookup_attr'), [
397            ast.Name('data'), self.visit(node.expr, locals_=locals_),
398            ast.Const(node.attrname)
399        ])
400
401    def visitLambda(self, node, locals_=False):
402        node.code = self.visit(node.code, locals_=True)
403        node.filename = '<string>' # workaround for bug in pycodegen
404        return node
405
406    def visitListComp(self, node, locals_=False):
407        node.expr = self.visit(node.expr, locals_=True)
408        node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
409        return node
410
411    def visitName(self, node, locals_=False):
412        func_args = [ast.Name('data'), ast.Const(node.name)]
413        if locals_:
414            func_args.append(ast.CallFunc(ast.Name('locals'), []))
415        return ast.CallFunc(ast.Name('_lookup_name'), func_args)
416
417    def visitSubscript(self, node, locals_=False):
418        return ast.CallFunc(ast.Name('_lookup_item'), [
419            ast.Name('data'), self.visit(node.expr, locals_=locals_),
420            ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
421        ])
Note: See TracBrowser for help on using the repository browser.