Edgewall Software

source: tags/0.3.1/genshi/eval.py

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

Fixed EOL style.

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