Edgewall Software

source: branches/stable/0.4.x/genshi/template/eval.py

Last change on this file was 701, checked in by cmlenz, 16 years ago

Ported [700] to 0.4.x branch.

  • Property svn:eol-style set to native
File size: 23.7 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 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, ModuleCodeGenerator
19import new
20try:
21    set
22except NameError:
23    from sets import Set as set
24import sys
25
26from genshi.core import Markup
27from genshi.template.base import TemplateRuntimeError
28from genshi.util import flatten
29
30__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
31           'Undefined', 'UndefinedError']
32__docformat__ = 'restructuredtext en'
33
34
35class Code(object):
36    """Abstract base class for the `Expression` and `Suite` classes."""
37    __slots__ = ['source', 'code', '_globals']
38
39    def __init__(self, source, filename=None, lineno=-1, lookup='lenient'):
40        """Create the code object, either from a string, or from an AST node.
41       
42        :param source: either a string containing the source code, or an AST
43                       node
44        :param filename: the (preferably absolute) name of the file containing
45                         the code
46        :param lineno: the number of the line on which the code was found
47        :param lookup: the lookup class that defines how variables are looked
48                       up in the context. Can be either `LenientLookup` (the
49                       default), `StrictLookup`, or a custom lookup class
50        """
51        if isinstance(source, basestring):
52            self.source = source
53            node = _parse(source, mode=self.mode)
54        else:
55            assert isinstance(source, ast.Node)
56            self.source = '?'
57            if self.mode == 'eval':
58                node = ast.Expression(source)
59            else:
60                node = ast.Module(None, source)
61
62        self.code = _compile(node, self.source, mode=self.mode,
63                             filename=filename, lineno=lineno)
64        if lookup is None:
65            lookup = LenientLookup
66        elif isinstance(lookup, basestring):
67            lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
68        self._globals = lookup.globals()
69
70    def __eq__(self, other):
71        return (type(other) == type(self)) and (self.code == other.code)
72
73    def __hash__(self):
74        return hash(self.code)
75
76    def __ne__(self, other):
77        return not self == other
78
79    def __repr__(self):
80        return '%s(%r)' % (self.__class__.__name__, self.source)
81
82
83class Expression(Code):
84    """Evaluates Python expressions used in templates.
85
86    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
87    >>> Expression('test').evaluate(data)
88    'Foo'
89
90    >>> Expression('items[0]').evaluate(data)
91    1
92    >>> Expression('items[-1]').evaluate(data)
93    3
94    >>> Expression('dict["some"]').evaluate(data)
95    'thing'
96   
97    Similar to e.g. Javascript, expressions in templates can use the dot
98    notation for attribute access to access items in mappings:
99   
100    >>> Expression('dict.some').evaluate(data)
101    'thing'
102   
103    This also works the other way around: item access can be used to access
104    any object attribute:
105   
106    >>> class MyClass(object):
107    ...     myattr = 'Bar'
108    >>> data = dict(mine=MyClass(), key='myattr')
109    >>> Expression('mine.myattr').evaluate(data)
110    'Bar'
111    >>> Expression('mine["myattr"]').evaluate(data)
112    'Bar'
113    >>> Expression('mine[key]').evaluate(data)
114    'Bar'
115   
116    All of the standard Python operators are available to template expressions.
117    Built-in functions such as ``len()`` are also available in template
118    expressions:
119   
120    >>> data = dict(items=[1, 2, 3])
121    >>> Expression('len(items)').evaluate(data)
122    3
123    """
124    __slots__ = []
125    mode = 'eval'
126
127    def evaluate(self, data):
128        """Evaluate the expression against the given data dictionary.
129       
130        :param data: a mapping containing the data to evaluate against
131        :return: the result of the evaluation
132        """
133        __traceback_hide__ = 'before_and_this'
134        _globals = self._globals
135        _globals['data'] = data
136        return eval(self.code, _globals, {'data': data})
137
138
139class Suite(Code):
140    """Executes Python statements used in templates.
141
142    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
143    >>> Suite("foo = dict['some']").execute(data)
144    >>> data['foo']
145    'thing'
146    """
147    __slots__ = []
148    mode = 'exec'
149
150    def execute(self, data):
151        """Execute the suite in the given data dictionary.
152       
153        :param data: a mapping containing the data to execute in
154        """
155        __traceback_hide__ = 'before_and_this'
156        _globals = self._globals
157        _globals['data'] = data
158        exec self.code in _globals, data
159
160
161UNDEFINED = object()
162
163
164class UndefinedError(TemplateRuntimeError):
165    """Exception thrown when a template expression attempts to access a variable
166    not defined in the context.
167   
168    :see: `LenientLookup`, `StrictLookup`
169    """
170    def __init__(self, name, owner=UNDEFINED):
171        if owner is not UNDEFINED:
172            message = '%s has no member named "%s"' % (repr(owner), name)
173        else:
174            message = '"%s" not defined' % name
175        TemplateRuntimeError.__init__(self, message)
176
177
178class Undefined(object):
179    """Represents a reference to an undefined variable.
180   
181    Unlike the Python runtime, template expressions can refer to an undefined
182    variable without causing a `NameError` to be raised. The result will be an
183    instance of the `Undefined` class, which is treated the same as ``False`` in
184    conditions, but raise an exception on any other operation:
185   
186    >>> foo = Undefined('foo')
187    >>> bool(foo)
188    False
189    >>> list(foo)
190    []
191    >>> print foo
192    undefined
193   
194    However, calling an undefined variable, or trying to access an attribute
195    of that variable, will raise an exception that includes the name used to
196    reference that undefined variable.
197   
198    >>> foo('bar')
199    Traceback (most recent call last):
200        ...
201    UndefinedError: "foo" not defined
202
203    >>> foo.bar
204    Traceback (most recent call last):
205        ...
206    UndefinedError: "foo" not defined
207   
208    :see: `LenientLookup`
209    """
210    __slots__ = ['_name', '_owner']
211
212    def __init__(self, name, owner=UNDEFINED):
213        """Initialize the object.
214       
215        :param name: the name of the reference
216        :param owner: the owning object, if the variable is accessed as a member
217        """
218        self._name = name
219        self._owner = owner
220
221    def __iter__(self):
222        return iter([])
223
224    def __nonzero__(self):
225        return False
226
227    def __repr__(self):
228        return '<%s %r>' % (self.__class__.__name__, self._name)
229
230    def __str__(self):
231        return 'undefined'
232
233    def _die(self, *args, **kwargs):
234        """Raise an `UndefinedError`."""
235        __traceback_hide__ = True
236        raise UndefinedError(self._name, self._owner)
237    __call__ = __getattr__ = __getitem__ = _die
238
239
240class LookupBase(object):
241    """Abstract base class for variable lookup implementations."""
242
243    def globals(cls):
244        """Construct the globals dictionary to use as the execution context for
245        the expression or suite.
246        """
247        return {
248            '_lookup_name': cls.lookup_name,
249            '_lookup_attr': cls.lookup_attr,
250            '_lookup_item': cls.lookup_item,
251            'UndefinedError': UndefinedError
252        }
253    globals = classmethod(globals)
254
255    def lookup_name(cls, data, name):
256        __traceback_hide__ = True
257        val = data.get(name, UNDEFINED)
258        if val is UNDEFINED:
259            val = BUILTINS.get(name, val)
260            if val is UNDEFINED:
261                val = cls.undefined(name)
262        return val
263    lookup_name = classmethod(lookup_name)
264
265    def lookup_attr(cls, data, obj, key):
266        __traceback_hide__ = True
267        val = getattr(obj, key, UNDEFINED)
268        if val is UNDEFINED:
269            try:
270                val = obj[key]
271            except (KeyError, TypeError):
272                val = cls.undefined(key, owner=obj)
273        return val
274    lookup_attr = classmethod(lookup_attr)
275
276    def lookup_item(cls, data, obj, key):
277        __traceback_hide__ = True
278        if len(key) == 1:
279            key = key[0]
280        try:
281            return obj[key]
282        except (AttributeError, KeyError, IndexError, TypeError), e:
283            if isinstance(key, basestring):
284                val = getattr(obj, key, UNDEFINED)
285                if val is UNDEFINED:
286                    val = cls.undefined(key, owner=obj)
287                return val
288            raise
289    lookup_item = classmethod(lookup_item)
290
291    def undefined(cls, key, owner=UNDEFINED):
292        """Can be overridden by subclasses to specify behavior when undefined
293        variables are accessed.
294       
295        :param key: the name of the variable
296        :param owner: the owning object, if the variable is accessed as a member
297        """
298        raise NotImplementedError
299    undefined = classmethod(undefined)
300
301
302class LenientLookup(LookupBase):
303    """Default variable lookup mechanism for expressions.
304   
305    When an undefined variable is referenced using this lookup style, the
306    reference evaluates to an instance of the `Undefined` class:
307   
308    >>> expr = Expression('nothing', lookup='lenient')
309    >>> undef = expr.evaluate({})
310    >>> undef
311    <Undefined 'nothing'>
312   
313    The same will happen when a non-existing attribute or item is accessed on
314    an existing object:
315   
316    >>> expr = Expression('something.nil', lookup='lenient')
317    >>> expr.evaluate({'something': dict()})
318    <Undefined 'nil'>
319   
320    See the documentation of the `Undefined` class for details on the behavior
321    of such objects.
322   
323    :see: `StrictLookup`
324    """
325    def undefined(cls, key, owner=UNDEFINED):
326        """Return an ``Undefined`` object."""
327        __traceback_hide__ = True
328        return Undefined(key, owner=owner)
329    undefined = classmethod(undefined)
330
331
332class StrictLookup(LookupBase):
333    """Strict variable lookup mechanism for expressions.
334   
335    Referencing an undefined variable using this lookup style will immediately
336    raise an ``UndefinedError``:
337   
338    >>> expr = Expression('nothing', lookup='strict')
339    >>> expr.evaluate({})
340    Traceback (most recent call last):
341        ...
342    UndefinedError: "nothing" not defined
343   
344    The same happens when a non-existing attribute or item is accessed on an
345    existing object:
346   
347    >>> expr = Expression('something.nil', lookup='strict')
348    >>> expr.evaluate({'something': dict()})
349    Traceback (most recent call last):
350        ...
351    UndefinedError: {} has no member named "nil"
352    """
353    def undefined(cls, key, owner=UNDEFINED):
354        """Raise an ``UndefinedError`` immediately."""
355        __traceback_hide__ = True
356        raise UndefinedError(key, owner=owner)
357    undefined = classmethod(undefined)
358
359
360def _parse(source, mode='eval'):
361    if isinstance(source, unicode):
362        source = '\xef\xbb\xbf' + source.encode('utf-8')
363    return parse(source, mode)
364
365def _compile(node, source=None, mode='eval', filename=None, lineno=-1):
366    xform = {'eval': ExpressionASTTransformer}.get(mode, TemplateASTTransformer)
367    tree = xform().visit(node)
368    if isinstance(filename, unicode):
369        # unicode file names not allowed for code objects
370        filename = filename.encode('utf-8', 'replace')
371    elif not filename:
372        filename = '<string>'
373    tree.filename = filename
374    if lineno <= 0:
375        lineno = 1
376
377    if mode == 'eval':
378        gen = ExpressionCodeGenerator(tree)
379        name = '<Expression %s>' % (repr(source or '?'))
380    else:
381        gen = ModuleCodeGenerator(tree)
382        name = '<Suite>'
383    gen.optimized = True
384    code = gen.getCode()
385
386    # We'd like to just set co_firstlineno, but it's readonly. So we need to
387    # clone the code object while adjusting the line number
388    return new.code(0, code.co_nlocals, code.co_stacksize,
389                    code.co_flags | 0x0040, code.co_code, code.co_consts,
390                    code.co_names, code.co_varnames, filename, name, lineno,
391                    code.co_lnotab, (), ())
392
393BUILTINS = __builtin__.__dict__.copy()
394BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
395
396
397class ASTTransformer(object):
398    """General purpose base class for AST transformations.
399   
400    Every visitor method can be overridden to return an AST node that has been
401    altered or replaced in some way.
402    """
403
404    def visit(self, node):
405        if node is None:
406            return None
407        if type(node) is tuple:
408            return tuple([self.visit(n) for n in node])
409        visitor = getattr(self, 'visit%s' % node.__class__.__name__,
410                          self._visitDefault)
411        return visitor(node)
412
413    def _visitDefault(self, node):
414        return node
415
416    def visitExpression(self, node):
417        node.node = self.visit(node.node)
418        return node
419
420    def visitModule(self, node):
421        node.node = self.visit(node.node)
422        return node
423
424    def visitStmt(self, node):
425        node.nodes = [self.visit(x) for x in node.nodes]
426        return node
427
428    # Classes, Functions & Accessors
429
430    def visitCallFunc(self, node):
431        node.node = self.visit(node.node)
432        node.args = [self.visit(x) for x in node.args]
433        if node.star_args:
434            node.star_args = self.visit(node.star_args)
435        if node.dstar_args:
436            node.dstar_args = self.visit(node.dstar_args)
437        return node
438
439    def visitClass(self, node):
440        node.bases = [self.visit(x) for x in node.bases]
441        node.code = self.visit(node.code)
442        node.filename = '<string>' # workaround for bug in pycodegen
443        return node
444
445    def visitFunction(self, node):
446        if hasattr(node, 'decorators'):
447            node.decorators = self.visit(node.decorators)
448        node.defaults = [self.visit(x) for x in node.defaults]
449        node.code = self.visit(node.code)
450        node.filename = '<string>' # workaround for bug in pycodegen
451        return node
452
453    def visitGetattr(self, node):
454        node.expr = self.visit(node.expr)
455        return node
456
457    def visitLambda(self, node):
458        node.code = self.visit(node.code)
459        node.filename = '<string>' # workaround for bug in pycodegen
460        return node
461
462    def visitSubscript(self, node):
463        node.expr = self.visit(node.expr)
464        node.subs = [self.visit(x) for x in node.subs]
465        return node
466
467    # Statements
468
469    def visitAssert(self, node):
470        node.test = self.visit(node.test)
471        node.fail = self.visit(node.fail)
472        return node
473
474    def visitAssign(self, node):
475        node.nodes = [self.visit(x) for x in node.nodes]
476        node.expr = self.visit(node.expr)
477        return node
478
479    def visitAssAttr(self, node):
480        node.expr = self.visit(node.expr)
481        return node
482
483    def visitAugAssign(self, node):
484        node.node = self.visit(node.node)
485        node.expr = self.visit(node.expr)
486        return node
487
488    def visitDecorators(self, node):
489        node.nodes = [self.visit(x) for x in node.nodes]
490        return node
491
492    def visitExec(self, node):
493        node.expr = self.visit(node.expr)
494        node.locals = self.visit(node.locals)
495        node.globals = self.visit(node.globals)
496        return node
497
498    def visitFor(self, node):
499        node.assign = self.visit(node.assign)
500        node.list = self.visit(node.list)
501        node.body = self.visit(node.body)
502        node.else_ = self.visit(node.else_)
503        return node
504
505    def visitIf(self, node):
506        node.tests = [self.visit(x) for x in node.tests]
507        node.else_ = self.visit(node.else_)
508        return node
509
510    def _visitPrint(self, node):
511        node.nodes = [self.visit(x) for x in node.nodes]
512        node.dest = self.visit(node.dest)
513        return node
514    visitPrint = visitPrintnl = _visitPrint
515
516    def visitRaise(self, node):
517        node.expr1 = self.visit(node.expr1)
518        node.expr2 = self.visit(node.expr2)
519        node.expr3 = self.visit(node.expr3)
520        return node
521
522    def visitReturn(self, node):
523        node.value = self.visit(node.value)
524        return node
525
526    def visitTryExcept(self, node):
527        node.body = self.visit(node.body)
528        node.handlers = self.visit(node.handlers)
529        node.else_ = self.visit(node.else_)
530        return node
531
532    def visitTryFinally(self, node):
533        node.body = self.visit(node.body)
534        node.final = self.visit(node.final)
535        return node
536
537    def visitWhile(self, node):
538        node.test = self.visit(node.test)
539        node.body = self.visit(node.body)
540        node.else_ = self.visit(node.else_)
541        return node
542
543    def visitWith(self, node):
544        node.expr = self.visit(node.expr)
545        node.vars = [self.visit(x) for x in node.vars]
546        node.body = self.visit(node.body)
547        return node
548
549    def visitYield(self, node):
550        node.value = self.visit(node.value)
551        return node
552
553    # Operators
554
555    def _visitBoolOp(self, node):
556        node.nodes = [self.visit(x) for x in node.nodes]
557        return node
558    visitAnd = visitOr = visitBitand = visitBitor = visitBitxor = _visitBoolOp
559    visitAssTuple = visitAssList = _visitBoolOp
560
561    def _visitBinOp(self, node):
562        node.left = self.visit(node.left)
563        node.right = self.visit(node.right)
564        return node
565    visitAdd = visitSub = _visitBinOp
566    visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp
567    visitLeftShift = visitRightShift = _visitBinOp
568
569    def visitCompare(self, node):
570        node.expr = self.visit(node.expr)
571        node.ops = [(op, self.visit(n)) for op, n in  node.ops]
572        return node
573
574    def _visitUnaryOp(self, node):
575        node.expr = self.visit(node.expr)
576        return node
577    visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp
578    visitBackquote = visitDiscard = _visitUnaryOp
579
580    def visitIfExp(self, node):
581        node.test = self.visit(node.test)
582        node.then = self.visit(node.then)
583        node.else_ = self.visit(node.else_)
584        return node
585
586    # Identifiers, Literals and Comprehensions
587
588    def visitDict(self, node):
589        node.items = [(self.visit(k),
590                       self.visit(v)) for k, v in node.items]
591        return node
592
593    def visitGenExpr(self, node):
594        node.code = self.visit(node.code)
595        node.filename = '<string>' # workaround for bug in pycodegen
596        return node
597
598    def visitGenExprFor(self, node):
599        node.assign = self.visit(node.assign)
600        node.iter = self.visit(node.iter)
601        node.ifs = [self.visit(x) for x in node.ifs]
602        return node
603
604    def visitGenExprIf(self, node):
605        node.test = self.visit(node.test)
606        return node
607
608    def visitGenExprInner(self, node):
609        node.quals = [self.visit(x) for x in node.quals]
610        node.expr = self.visit(node.expr)
611        return node
612
613    def visitKeyword(self, node):
614        node.expr = self.visit(node.expr)
615        return node
616
617    def visitList(self, node):
618        node.nodes = [self.visit(n) for n in node.nodes]
619        return node
620
621    def visitListComp(self, node):
622        node.quals = [self.visit(x) for x in node.quals]
623        node.expr = self.visit(node.expr)
624        return node
625
626    def visitListCompFor(self, node):
627        node.assign = self.visit(node.assign)
628        node.list = self.visit(node.list)
629        node.ifs = [self.visit(x) for x in node.ifs]
630        return node
631
632    def visitListCompIf(self, node):
633        node.test = self.visit(node.test)
634        return node
635
636    def visitSlice(self, node):
637        node.expr = self.visit(node.expr)
638        if node.lower is not None:
639            node.lower = self.visit(node.lower)
640        if node.upper is not None:
641            node.upper = self.visit(node.upper)
642        return node
643
644    def visitSliceobj(self, node):
645        node.nodes = [self.visit(x) for x in node.nodes]
646        return node
647
648    def visitTuple(self, node):
649        node.nodes = [self.visit(n) for n in node.nodes]
650        return node
651
652
653class TemplateASTTransformer(ASTTransformer):
654    """Concrete AST transformer that implements the AST transformations needed
655    for code embedded in templates.
656    """
657
658    def __init__(self):
659        self.locals = []
660
661    def visitConst(self, node):
662        if isinstance(node.value, str):
663            try: # If the string is ASCII, return a `str` object
664                node.value.decode('ascii')
665            except ValueError: # Otherwise return a `unicode` object
666                return ast.Const(node.value.decode('utf-8'))
667        return node
668
669    def visitAssName(self, node):
670        if self.locals:
671            self.locals[-1].add(node.name)
672        return node
673
674    def visitAugAssign(self, node):
675        if isinstance(node.node, ast.Name) and (not self.locals
676                or node.node.name not in flatten(self.locals)):
677            name = node.node.name
678            node.node = ast.Subscript(ast.Name('data'), 'OP_APPLY',
679                                      [ast.Const(name)])
680            node.expr = self.visit(node.expr)
681            return ast.If([
682                (ast.Compare(ast.Const(name), [('in', ast.Name('data'))]),
683                 ast.Stmt([node]))],
684                ast.Stmt([ast.Raise(ast.CallFunc(ast.Name('UndefinedError'),
685                                                 [ast.Const(name)]),
686                                    None, None)]))
687        else:
688            return ASTTransformer.visitAugAssign(self, node)
689
690    def visitClass(self, node):
691        if self.locals:
692            self.locals[-1].add(node.name)
693        self.locals.append(set())
694        node = ASTTransformer.visitClass(self, node)
695        self.locals.pop()
696        return node
697
698    def visitFor(self, node):
699        self.locals.append(set())
700        node = ASTTransformer.visitFor(self, node)
701        self.locals.pop()
702        return node
703
704    def visitFunction(self, node):
705        if self.locals:
706            self.locals[-1].add(node.name)
707        self.locals.append(set(node.argnames))
708        node = ASTTransformer.visitFunction(self, node)
709        self.locals.pop()
710        return node
711
712    def visitGenExpr(self, node):
713        self.locals.append(set())
714        node = ASTTransformer.visitGenExpr(self, node)
715        self.locals.pop()
716        return node
717
718    def visitLambda(self, node):
719        self.locals.append(set(flatten(node.argnames)))
720        node = ASTTransformer.visitLambda(self, node)
721        self.locals.pop()
722        return node
723
724    def visitListComp(self, node):
725        self.locals.append(set())
726        node = ASTTransformer.visitListComp(self, node)
727        self.locals.pop()
728        return node
729
730    def visitName(self, node):
731        # If the name refers to a local inside a lambda, list comprehension, or
732        # generator expression, leave it alone
733        if node.name not in flatten(self.locals):
734            # Otherwise, translate the name ref into a context lookup
735            func_args = [ast.Name('data'), ast.Const(node.name)]
736            node = ast.CallFunc(ast.Name('_lookup_name'), func_args)
737        return node
738
739
740class ExpressionASTTransformer(TemplateASTTransformer):
741    """Concrete AST transformer that implements the AST transformations needed
742    for code embedded in templates.
743    """
744
745    def visitGetattr(self, node):
746        return ast.CallFunc(ast.Name('_lookup_attr'), [
747            ast.Name('data'), self.visit(node.expr),
748            ast.Const(node.attrname)
749        ])
750
751    def visitSubscript(self, node):
752        return ast.CallFunc(ast.Name('_lookup_item'), [
753            ast.Name('data'), self.visit(node.expr),
754            ast.Tuple([self.visit(sub) for sub in node.subs])
755        ])
Note: See TracBrowser for help on using the repository browser.