Edgewall Software

source: tags/0.3.4/genshi/eval.py

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

Create tag for 0.3.4 release.

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