Edgewall Software

source: tags/0.3.3/genshi/eval.py

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

Ported [330],[333], and [334] to 0.3.x stable branch.

  • Property svn:eol-style set to native
File size: 13.8 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    __slots__ = ['source', 'code']
66
67    def __init__(self, source, filename=None, lineno=-1):
68        if isinstance(source, basestring):
69            self.source = source
70            if isinstance(source, unicode):
71                source = '\xef\xbb\xbf' + source.encode('utf-8')
72            self.code = _compile(parse(source, 'eval'), self.source,
73                                 filename=filename, lineno=lineno)
74        else:
75            assert isinstance(source, ast.Node)
76            self.source = '?'
77            self.code = _compile(ast.Expression(source), filename=filename,
78                                 lineno=lineno)
79
80    def __repr__(self):
81        return '<Expression "%s">' % self.source
82
83    def evaluate(self, data, nocall=False):
84        """Evaluate the expression against the given data dictionary.
85       
86        @param data: a mapping containing the data to evaluate against
87        @param nocall: if true, the result of the evaluation is not called if
88            if it is a callable
89        @return: the result of the evaluation
90        """
91        retval = eval(self.code, {'data': data,
92                                  '_lookup_name': _lookup_name,
93                                  '_lookup_attr': _lookup_attr,
94                                  '_lookup_item': _lookup_item},
95                                 {'data': data})
96        if not nocall and type(retval) is not Undefined and callable(retval):
97            retval = retval()
98        return retval
99
100
101class Undefined(object):
102    """Represents a reference to an undefined variable.
103   
104    Unlike the Python runtime, template expressions can refer to an undefined
105    variable without causing a `NameError` to be raised. The result will be an
106    instance of the `Undefined´ class, which is treated the same as `False` in
107    conditions, and acts as an empty collection in iterations:
108   
109    >>> foo = Undefined('foo')
110    >>> bool(foo)
111    False
112    >>> list(foo)
113    []
114    >>> print foo
115    undefined
116   
117    However, calling an undefined variable, or trying to access an attribute
118    of that variable, will raise an exception that includes the name used to
119    reference that undefined variable.
120   
121    >>> foo('bar')
122    Traceback (most recent call last):
123        ...
124    NameError: Variable "foo" is not defined
125
126    >>> foo.bar
127    Traceback (most recent call last):
128        ...
129    NameError: Variable "foo" is not defined
130    """
131    __slots__ = ['name']
132
133    def __init__(self, name):
134        self.name = name
135
136    def __call__(self, *args, **kwargs):
137        self.throw()
138
139    def __getattr__(self, name):
140        self.throw()
141
142    def __iter__(self):
143        return iter([])
144
145    def __nonzero__(self):
146        return False
147
148    def __repr__(self):
149        return 'undefined'
150
151    def throw(self):
152        raise NameError('Variable "%s" is not defined' % self.name)
153
154
155def _compile(node, source=None, filename=None, lineno=-1):
156    tree = ExpressionASTTransformer().visit(node)
157    if isinstance(filename, unicode):
158        # unicode file names not allowed for code objects
159        filename = filename.encode('utf-8', 'replace')
160    elif not filename:
161        filename = '<string>'
162    tree.filename = filename
163    if lineno <= 0:
164        lineno = 1
165
166    gen = ExpressionCodeGenerator(tree)
167    gen.optimized = True
168    code = gen.getCode()
169
170    # We'd like to just set co_firstlineno, but it's readonly. So we need to
171    # clone the code object while adjusting the line number
172    return new.code(0, code.co_nlocals, code.co_stacksize,
173                    code.co_flags | 0x0040, code.co_code, code.co_consts,
174                    code.co_names, code.co_varnames, filename,
175                    '<Expression %s>' % (repr(source).replace("'", '"') or '?'),
176                    lineno, code.co_lnotab, (), ())
177
178BUILTINS = __builtin__.__dict__.copy()
179BUILTINS['Undefined'] = Undefined
180
181def _lookup_name(data, name, locals_=None):
182    val = Undefined
183    if locals_:
184        val = locals_.get(name, val)
185    if val is Undefined:
186        val = data.get(name, val)
187        if val is Undefined:
188            val = BUILTINS.get(name, val)
189            if val is not Undefined or name == 'Undefined':
190                return val
191        else:
192            return val
193    else:
194        return val
195    return val(name)
196
197def _lookup_attr(data, obj, key):
198    if type(obj) is Undefined:
199        obj.throw()
200    if hasattr(obj, key):
201        return getattr(obj, key)
202    try:
203        return obj[key]
204    except (KeyError, TypeError):
205        return None
206
207def _lookup_item(data, obj, key):
208    if type(obj) is Undefined:
209        obj.throw()
210    if len(key) == 1:
211        key = key[0]
212    try:
213        return obj[key]
214    except (KeyError, IndexError, TypeError), e:
215        if isinstance(key, basestring):
216            try:
217                return getattr(obj, key)
218            except (AttributeError, TypeError), e:
219                pass
220
221
222class ASTTransformer(object):
223    """General purpose base class for AST transformations.
224   
225    Every visitor method can be overridden to return an AST node that has been
226    altered or replaced in some way.
227    """
228    _visitors = {}
229
230    def visit(self, node, *args, **kwargs):
231        v = self._visitors.get(node.__class__)
232        if not v:
233            v = getattr(self, 'visit%s' % node.__class__.__name__)
234            self._visitors[node.__class__] = v
235        return v(node, *args, **kwargs)
236
237    def visitExpression(self, node, *args, **kwargs):
238        node.node = self.visit(node.node, *args, **kwargs)
239        return node
240
241    # Functions & Accessors
242
243    def visitCallFunc(self, node, *args, **kwargs):
244        node.node = self.visit(node.node, *args, **kwargs)
245        node.args = [self.visit(x, *args, **kwargs) for x in node.args]
246        if node.star_args:
247            node.star_args = self.visit(node.star_args, *args, **kwargs)
248        if node.dstar_args:
249            node.dstar_args = self.visit(node.dstar_args, *args, **kwargs)
250        return node
251
252    def visitLambda(self, node, *args, **kwargs):
253        node.code = self.visit(node.code, *args, **kwargs)
254        node.filename = '<string>' # workaround for bug in pycodegen
255        return node
256
257    def visitGetattr(self, node, *args, **kwargs):
258        node.expr = self.visit(node.expr, *args, **kwargs)
259        return node
260
261    def visitSubscript(self, node, *args, **kwargs):
262        node.expr = self.visit(node.expr, *args, **kwargs)
263        node.subs = [self.visit(x, *args, **kwargs) for x in node.subs]
264        return node
265
266    # Operators
267
268    def _visitBoolOp(self, node, *args, **kwargs):
269        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
270        return node
271    visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp
272
273    def _visitBinOp(self, node, *args, **kwargs):
274        node.left = self.visit(node.left, *args, **kwargs)
275        node.right = self.visit(node.right, *args, **kwargs)
276        return node
277    visitAdd = visitSub = _visitBinOp
278    visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
279    visitLeftShift = visitRightShift = _visitBinOp
280
281    def visitCompare(self, node, *args, **kwargs):
282        node.expr = self.visit(node.expr, *args, **kwargs)
283        node.ops = [(op, self.visit(n, *args, **kwargs)) for op, n in  node.ops]
284        return node
285
286    def _visitUnaryOp(self, node, *args, **kwargs):
287        node.expr = self.visit(node.expr, *args, **kwargs)
288        return node
289    visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
290    visitBackquote = _visitUnaryOp
291
292    # Identifiers, Literals and Comprehensions
293
294    def _visitDefault(self, node, *args, **kwargs):
295        return node
296    visitAssName = visitAssTuple = _visitDefault
297    visitConst = visitName = _visitDefault
298
299    def visitDict(self, node, *args, **kwargs):
300        node.items = [(self.visit(k, *args, **kwargs),
301                       self.visit(v, *args, **kwargs)) for k, v in node.items]
302        return node
303
304    def visitGenExpr(self, node, *args, **kwargs):
305        node.code = self.visit(node.code, *args, **kwargs)
306        node.filename = '<string>' # workaround for bug in pycodegen
307        return node
308
309    def visitGenExprFor(self, node, *args, **kwargs):
310        node.assign = self.visit(node.assign, *args, **kwargs)
311        node.iter = self.visit(node.iter, *args, **kwargs)
312        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
313        return node
314
315    def visitGenExprIf(self, node, *args, **kwargs):
316        node.test = self.visit(node.test, *args, **kwargs)
317        return node
318
319    def visitGenExprInner(self, node, *args, **kwargs):
320        node.expr = self.visit(node.expr, *args, **kwargs)
321        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
322        return node
323
324    def visitKeyword(self, node, *args, **kwargs):
325        node.expr = self.visit(node.expr, *args, **kwargs)
326        return node
327
328    def visitList(self, node, *args, **kwargs):
329        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
330        return node
331
332    def visitListComp(self, node, *args, **kwargs):
333        node.expr = self.visit(node.expr, *args, **kwargs)
334        node.quals = [self.visit(x, *args, **kwargs) for x in node.quals]
335        return node
336
337    def visitListCompFor(self, node, *args, **kwargs):
338        node.assign = self.visit(node.assign, *args, **kwargs)
339        node.list = self.visit(node.list, *args, **kwargs)
340        node.ifs = [self.visit(x, *args, **kwargs) for x in node.ifs]
341        return node
342
343    def visitListCompIf(self, node, *args, **kwargs):
344        node.test = self.visit(node.test, *args, **kwargs)
345        return node
346
347    def visitSlice(self, node, *args, **kwargs):
348        node.expr = self.visit(node.expr, locals_=True, *args, **kwargs)
349        if node.lower is not None:
350            node.lower = self.visit(node.lower, *args, **kwargs)
351        if node.upper is not None:
352            node.upper = self.visit(node.upper, *args, **kwargs)
353        return node
354
355    def visitSliceobj(self, node, *args, **kwargs):
356        node.nodes = [self.visit(x, *args, **kwargs) for x in node.nodes]
357        return node
358
359    def visitTuple(self, node, *args, **kwargs):
360        node.nodes = [self.visit(n, *args, **kwargs) for n in node.nodes]
361        return node
362
363
364class ExpressionASTTransformer(ASTTransformer):
365    """Concrete AST transformer that implements the AST transformations needed
366    for template expressions.
367    """
368
369    def visitConst(self, node, locals_=False):
370        if isinstance(node.value, str):
371            return ast.Const(node.value.decode('utf-8'))
372        return node
373
374    def visitGenExprIf(self, node, *args, **kwargs):
375        node.test = self.visit(node.test, locals_=True)
376        return node
377
378    def visitGenExprInner(self, node, *args, **kwargs):
379        node.expr = self.visit(node.expr, locals_=True)
380        node.quals = [self.visit(x) for x in node.quals]
381        return node
382
383    def visitGetattr(self, node, locals_=False):
384        return ast.CallFunc(ast.Name('_lookup_attr'), [
385            ast.Name('data'), self.visit(node.expr, locals_=locals_),
386            ast.Const(node.attrname)
387        ])
388
389    def visitLambda(self, node, locals_=False):
390        node.code = self.visit(node.code, locals_=True)
391        node.filename = '<string>' # workaround for bug in pycodegen
392        return node
393
394    def visitListComp(self, node, locals_=False):
395        node.expr = self.visit(node.expr, locals_=True)
396        node.quals = [self.visit(qual, locals_=True) for qual in node.quals]
397        return node
398
399    def visitName(self, node, locals_=False):
400        func_args = [ast.Name('data'), ast.Const(node.name)]
401        if locals_:
402            func_args.append(ast.CallFunc(ast.Name('locals'), []))
403        return ast.CallFunc(ast.Name('_lookup_name'), func_args)
404
405    def visitSubscript(self, node, locals_=False):
406        return ast.CallFunc(ast.Name('_lookup_item'), [
407            ast.Name('data'), self.visit(node.expr, locals_=locals_),
408            ast.Tuple([self.visit(sub, locals_=locals_) for sub in node.subs])
409        ])
Note: See TracBrowser for help on using the repository browser.