Edgewall Software

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

Last change on this file was 1120, checked in by cmlenz, 13 years ago

Update changelog and copyright years.

  • Property svn:eol-style set to native
File size: 20.6 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2010 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__
17
18from textwrap import dedent
19from types import CodeType
20
21from genshi.core import Markup
22from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, \
23                                    _ast, parse
24from genshi.template.base import TemplateRuntimeError
25from genshi.util import flatten
26
27__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
28           'Undefined', 'UndefinedError']
29__docformat__ = 'restructuredtext en'
30
31
32# Check for a Python 2.4 bug in the eval loop
33has_star_import_bug = False
34try:
35    class _FakeMapping(object):
36        __getitem__ = __setitem__ = lambda *a: None
37    exec 'from sys import *' in {}, _FakeMapping()
38except SystemError:
39    has_star_import_bug = True
40del _FakeMapping
41
42
43def _star_import_patch(mapping, modname):
44    """This function is used as helper if a Python version with a broken
45    star-import opcode is in use.
46    """
47    module = __import__(modname, None, None, ['__all__'])
48    if hasattr(module, '__all__'):
49        members = module.__all__
50    else:
51        members = [x for x in module.__dict__ if not x.startswith('_')]
52    mapping.update([(name, getattr(module, name)) for name in members])
53
54
55class Code(object):
56    """Abstract base class for the `Expression` and `Suite` classes."""
57    __slots__ = ['source', 'code', 'ast', '_globals']
58
59    def __init__(self, source, filename=None, lineno=-1, lookup='strict',
60                 xform=None):
61        """Create the code object, either from a string, or from an AST node.
62       
63        :param source: either a string containing the source code, or an AST
64                       node
65        :param filename: the (preferably absolute) name of the file containing
66                         the code
67        :param lineno: the number of the line on which the code was found
68        :param lookup: the lookup class that defines how variables are looked
69                       up in the context; can be either "strict" (the default),
70                       "lenient", or a custom lookup class
71        :param xform: the AST transformer that should be applied to the code;
72                      if `None`, the appropriate transformation is chosen
73                      depending on the mode
74        """
75        if isinstance(source, basestring):
76            self.source = source
77            node = _parse(source, mode=self.mode)
78        else:
79            assert isinstance(source, _ast.AST), \
80                'Expected string or AST node, but got %r' % source
81            self.source = '?'
82            if self.mode == 'eval':
83                node = _ast.Expression()
84                node.body = source
85            else:
86                node = _ast.Module()
87                node.body = [source]
88
89        self.ast = node
90        self.code = _compile(node, self.source, mode=self.mode,
91                             filename=filename, lineno=lineno, xform=xform)
92        if lookup is None:
93            lookup = LenientLookup
94        elif isinstance(lookup, basestring):
95            lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
96        self._globals = lookup.globals
97
98    def __getstate__(self):
99        state = {'source': self.source, 'ast': self.ast,
100                 'lookup': self._globals.im_self}
101        c = self.code
102        state['code'] = (c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code,
103                         c.co_consts, c.co_names, c.co_varnames, c.co_filename,
104                         c.co_name, c.co_firstlineno, c.co_lnotab, (), ())
105        return state
106
107    def __setstate__(self, state):
108        self.source = state['source']
109        self.ast = state['ast']
110        self.code = CodeType(0, *state['code'])
111        self._globals = state['lookup'].globals
112
113    def __eq__(self, other):
114        return (type(other) == type(self)) and (self.code == other.code)
115
116    def __hash__(self):
117        return hash(self.code)
118
119    def __ne__(self, other):
120        return not self == other
121
122    def __repr__(self):
123        return '%s(%r)' % (type(self).__name__, self.source)
124
125
126class Expression(Code):
127    """Evaluates Python expressions used in templates.
128
129    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
130    >>> Expression('test').evaluate(data)
131    'Foo'
132
133    >>> Expression('items[0]').evaluate(data)
134    1
135    >>> Expression('items[-1]').evaluate(data)
136    3
137    >>> Expression('dict["some"]').evaluate(data)
138    'thing'
139   
140    Similar to e.g. Javascript, expressions in templates can use the dot
141    notation for attribute access to access items in mappings:
142   
143    >>> Expression('dict.some').evaluate(data)
144    'thing'
145   
146    This also works the other way around: item access can be used to access
147    any object attribute:
148   
149    >>> class MyClass(object):
150    ...     myattr = 'Bar'
151    >>> data = dict(mine=MyClass(), key='myattr')
152    >>> Expression('mine.myattr').evaluate(data)
153    'Bar'
154    >>> Expression('mine["myattr"]').evaluate(data)
155    'Bar'
156    >>> Expression('mine[key]').evaluate(data)
157    'Bar'
158   
159    All of the standard Python operators are available to template expressions.
160    Built-in functions such as ``len()`` are also available in template
161    expressions:
162   
163    >>> data = dict(items=[1, 2, 3])
164    >>> Expression('len(items)').evaluate(data)
165    3
166    """
167    __slots__ = []
168    mode = 'eval'
169
170    def evaluate(self, data):
171        """Evaluate the expression against the given data dictionary.
172       
173        :param data: a mapping containing the data to evaluate against
174        :return: the result of the evaluation
175        """
176        __traceback_hide__ = 'before_and_this'
177        _globals = self._globals(data)
178        return eval(self.code, _globals, {'__data__': data})
179
180
181class Suite(Code):
182    """Executes Python statements used in templates.
183
184    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
185    >>> Suite("foo = dict['some']").execute(data)
186    >>> data['foo']
187    'thing'
188    """
189    __slots__ = []
190    mode = 'exec'
191
192    def execute(self, data):
193        """Execute the suite in the given data dictionary.
194       
195        :param data: a mapping containing the data to execute in
196        """
197        __traceback_hide__ = 'before_and_this'
198        _globals = self._globals(data)
199        exec self.code in _globals, data
200
201
202UNDEFINED = object()
203
204
205class UndefinedError(TemplateRuntimeError):
206    """Exception thrown when a template expression attempts to access a variable
207    not defined in the context.
208   
209    :see: `LenientLookup`, `StrictLookup`
210    """
211    def __init__(self, name, owner=UNDEFINED):
212        if owner is not UNDEFINED:
213            message = '%s has no member named "%s"' % (repr(owner), name)
214        else:
215            message = '"%s" not defined' % name
216        TemplateRuntimeError.__init__(self, message)
217
218
219class Undefined(object):
220    """Represents a reference to an undefined variable.
221   
222    Unlike the Python runtime, template expressions can refer to an undefined
223    variable without causing a `NameError` to be raised. The result will be an
224    instance of the `Undefined` class, which is treated the same as ``False`` in
225    conditions, but raise an exception on any other operation:
226   
227    >>> foo = Undefined('foo')
228    >>> bool(foo)
229    False
230    >>> list(foo)
231    []
232    >>> print(foo)
233    undefined
234   
235    However, calling an undefined variable, or trying to access an attribute
236    of that variable, will raise an exception that includes the name used to
237    reference that undefined variable.
238   
239    >>> foo('bar')
240    Traceback (most recent call last):
241        ...
242    UndefinedError: "foo" not defined
243
244    >>> foo.bar
245    Traceback (most recent call last):
246        ...
247    UndefinedError: "foo" not defined
248   
249    :see: `LenientLookup`
250    """
251    __slots__ = ['_name', '_owner']
252
253    def __init__(self, name, owner=UNDEFINED):
254        """Initialize the object.
255       
256        :param name: the name of the reference
257        :param owner: the owning object, if the variable is accessed as a member
258        """
259        self._name = name
260        self._owner = owner
261
262    def __iter__(self):
263        return iter([])
264
265    def __nonzero__(self):
266        return False
267
268    def __repr__(self):
269        return '<%s %r>' % (type(self).__name__, self._name)
270
271    def __str__(self):
272        return 'undefined'
273
274    def _die(self, *args, **kwargs):
275        """Raise an `UndefinedError`."""
276        __traceback_hide__ = True
277        raise UndefinedError(self._name, self._owner)
278    __call__ = __getattr__ = __getitem__ = _die
279
280    # Hack around some behavior introduced in Python 2.6.2
281    # http://genshi.edgewall.org/ticket/324
282    __length_hint__ = None
283
284
285class LookupBase(object):
286    """Abstract base class for variable lookup implementations."""
287
288    @classmethod
289    def globals(cls, data):
290        """Construct the globals dictionary to use as the execution context for
291        the expression or suite.
292        """
293        return {
294            '__data__': data,
295            '_lookup_name': cls.lookup_name,
296            '_lookup_attr': cls.lookup_attr,
297            '_lookup_item': cls.lookup_item,
298            '_star_import_patch': _star_import_patch,
299            'UndefinedError': UndefinedError,
300        }
301
302    @classmethod
303    def lookup_name(cls, data, name):
304        __traceback_hide__ = True
305        val = data.get(name, UNDEFINED)
306        if val is UNDEFINED:
307            val = BUILTINS.get(name, val)
308            if val is UNDEFINED:
309                val = cls.undefined(name)
310        return val
311
312    @classmethod
313    def lookup_attr(cls, obj, key):
314        __traceback_hide__ = True
315        try:
316            val = getattr(obj, key)
317        except AttributeError:
318            if hasattr(obj.__class__, key):
319                raise
320            else:
321                try:
322                    val = obj[key]
323                except (KeyError, TypeError):
324                    val = cls.undefined(key, owner=obj)
325        return val
326
327    @classmethod
328    def lookup_item(cls, obj, key):
329        __traceback_hide__ = True
330        if len(key) == 1:
331            key = key[0]
332        try:
333            return obj[key]
334        except (AttributeError, KeyError, IndexError, TypeError), e:
335            if isinstance(key, basestring):
336                val = getattr(obj, key, UNDEFINED)
337                if val is UNDEFINED:
338                    val = cls.undefined(key, owner=obj)
339                return val
340            raise
341
342    @classmethod
343    def undefined(cls, key, owner=UNDEFINED):
344        """Can be overridden by subclasses to specify behavior when undefined
345        variables are accessed.
346       
347        :param key: the name of the variable
348        :param owner: the owning object, if the variable is accessed as a member
349        """
350        raise NotImplementedError
351
352
353class LenientLookup(LookupBase):
354    """Default variable lookup mechanism for expressions.
355   
356    When an undefined variable is referenced using this lookup style, the
357    reference evaluates to an instance of the `Undefined` class:
358   
359    >>> expr = Expression('nothing', lookup='lenient')
360    >>> undef = expr.evaluate({})
361    >>> undef
362    <Undefined 'nothing'>
363   
364    The same will happen when a non-existing attribute or item is accessed on
365    an existing object:
366   
367    >>> expr = Expression('something.nil', lookup='lenient')
368    >>> expr.evaluate({'something': dict()})
369    <Undefined 'nil'>
370   
371    See the documentation of the `Undefined` class for details on the behavior
372    of such objects.
373   
374    :see: `StrictLookup`
375    """
376
377    @classmethod
378    def undefined(cls, key, owner=UNDEFINED):
379        """Return an ``Undefined`` object."""
380        __traceback_hide__ = True
381        return Undefined(key, owner=owner)
382
383
384class StrictLookup(LookupBase):
385    """Strict variable lookup mechanism for expressions.
386   
387    Referencing an undefined variable using this lookup style will immediately
388    raise an ``UndefinedError``:
389   
390    >>> expr = Expression('nothing', lookup='strict')
391    >>> expr.evaluate({})
392    Traceback (most recent call last):
393        ...
394    UndefinedError: "nothing" not defined
395   
396    The same happens when a non-existing attribute or item is accessed on an
397    existing object:
398   
399    >>> expr = Expression('something.nil', lookup='strict')
400    >>> expr.evaluate({'something': dict()})
401    Traceback (most recent call last):
402        ...
403    UndefinedError: {} has no member named "nil"
404    """
405
406    @classmethod
407    def undefined(cls, key, owner=UNDEFINED):
408        """Raise an ``UndefinedError`` immediately."""
409        __traceback_hide__ = True
410        raise UndefinedError(key, owner=owner)
411
412
413def _parse(source, mode='eval'):
414    source = source.strip()
415    if mode == 'exec':
416        lines = [line.expandtabs() for line in source.splitlines()]
417        if lines:
418            first = lines[0]
419            rest = dedent('\n'.join(lines[1:])).rstrip()
420            if first.rstrip().endswith(':') and not rest[0].isspace():
421                rest = '\n'.join(['    %s' % line for line in rest.splitlines()])
422            source = '\n'.join([first, rest])
423    if isinstance(source, unicode):
424        source = '\xef\xbb\xbf' + source.encode('utf-8')
425    return parse(source, mode)
426
427
428def _compile(node, source=None, mode='eval', filename=None, lineno=-1,
429             xform=None):
430    if isinstance(filename, unicode):
431        # unicode file names not allowed for code objects
432        filename = filename.encode('utf-8', 'replace')
433    elif not filename:
434        filename = '<string>'
435    if lineno <= 0:
436        lineno = 1
437
438    if xform is None:
439        xform = {
440            'eval': ExpressionASTTransformer
441        }.get(mode, TemplateASTTransformer)
442    tree = xform().visit(node)
443
444    if mode == 'eval':
445        name = '<Expression %r>' % (source or '?')
446    else:
447        lines = source.splitlines()
448        if not lines:
449            extract = ''
450        else:
451            extract = lines[0]
452        if len(lines) > 1:
453            extract += ' ...'
454        name = '<Suite %r>' % (extract)
455    new_source = ASTCodeGenerator(tree).code
456    code = compile(new_source, filename, mode)
457
458    try:
459        # We'd like to just set co_firstlineno, but it's readonly. So we need
460        # to clone the code object while adjusting the line number
461        return CodeType(0, code.co_nlocals, code.co_stacksize,
462                        code.co_flags | 0x0040, code.co_code, code.co_consts,
463                        code.co_names, code.co_varnames, filename, name,
464                        lineno, code.co_lnotab, (), ())
465    except RuntimeError:
466        return code
467
468
469def _new(class_, *args, **kwargs):
470    ret = class_()
471    for attr, value in zip(ret._fields, args):
472        if attr in kwargs:
473            raise ValueError('Field set both in args and kwargs')
474        setattr(ret, attr, value)
475    for attr, value in kwargs:
476        setattr(ret, attr, value)
477    return ret
478
479
480BUILTINS = __builtin__.__dict__.copy()
481BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
482CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
483
484
485class TemplateASTTransformer(ASTTransformer):
486    """Concrete AST transformer that implements the AST transformations needed
487    for code embedded in templates.
488    """
489
490    def __init__(self):
491        self.locals = [CONSTANTS]
492
493    def _extract_names(self, node):
494        names = set()
495        def _process(node):
496            if isinstance(node, _ast.Name):
497                names.add(node.id)
498            elif isinstance(node, _ast.alias):
499                names.add(node.asname or node.name)
500            elif isinstance(node, _ast.Tuple):
501                for elt in node.elts:
502                    _process(elt)
503        if hasattr(node, 'args'):
504            for arg in node.args:
505                _process(arg)
506            if hasattr(node, 'vararg'):
507                names.add(node.vararg)
508            if hasattr(node, 'kwarg'):
509                names.add(node.kwarg)
510        elif hasattr(node, 'names'):
511            for elt in node.names:
512                _process(elt)
513        return names
514
515    def visit_Str(self, node):
516        if isinstance(node.s, str):
517            try: # If the string is ASCII, return a `str` object
518                node.s.decode('ascii')
519            except ValueError: # Otherwise return a `unicode` object
520                return _new(_ast.Str, node.s.decode('utf-8'))
521        return node
522
523    def visit_ClassDef(self, node):
524        if len(self.locals) > 1:
525            self.locals[-1].add(node.name)
526        self.locals.append(set())
527        try:
528            return ASTTransformer.visit_ClassDef(self, node)
529        finally:
530            self.locals.pop()
531
532    def visit_Import(self, node):
533        if len(self.locals) > 1:
534            self.locals[-1].update(self._extract_names(node))
535        return ASTTransformer.visit_Import(self, node)
536
537    def visit_ImportFrom(self, node):
538        if [a.name for a in node.names] == ['*']:
539            if has_star_import_bug:
540                # This is a Python 2.4 bug. Only if we have a broken Python
541                # version do we need to apply this hack
542                node = _new(_ast.Expr, _new(_ast.Call,
543                    _new(_ast.Name, '_star_import_patch'), [
544                        _new(_ast.Name, '__data__'),
545                        _new(_ast.Str, node.module)
546                    ], (), ()))
547            return node
548        if len(self.locals) > 1:
549            self.locals[-1].update(self._extract_names(node))
550        return ASTTransformer.visit_ImportFrom(self, node)
551
552    def visit_FunctionDef(self, node):
553        if len(self.locals) > 1:
554            self.locals[-1].add(node.name)
555
556        self.locals.append(self._extract_names(node.args))
557        try:
558            return ASTTransformer.visit_FunctionDef(self, node)
559        finally:
560            self.locals.pop()
561
562    # GeneratorExp(expr elt, comprehension* generators)
563    def visit_GeneratorExp(self, node):
564        gens = []
565        for generator in node.generators:
566            # comprehension = (expr target, expr iter, expr* ifs)
567            self.locals.append(set())
568            gen = _new(_ast.comprehension, self.visit(generator.target),
569                       self.visit(generator.iter),
570                       [self.visit(if_) for if_ in generator.ifs])
571            gens.append(gen)
572
573        # use node.__class__ to make it reusable as ListComp
574        ret = _new(node.__class__, self.visit(node.elt), gens)
575        #delete inserted locals
576        del self.locals[-len(node.generators):]
577        return ret
578
579    # ListComp(expr elt, comprehension* generators)
580    visit_ListComp = visit_GeneratorExp
581
582    def visit_Lambda(self, node):
583        self.locals.append(self._extract_names(node.args))
584        try:
585            return ASTTransformer.visit_Lambda(self, node)
586        finally:
587            self.locals.pop()
588
589    def visit_Name(self, node):
590        # If the name refers to a local inside a lambda, list comprehension, or
591        # generator expression, leave it alone
592        if isinstance(node.ctx, _ast.Load) and \
593                node.id not in flatten(self.locals):
594            # Otherwise, translate the name ref into a context lookup
595            name = _new(_ast.Name, '_lookup_name', _ast.Load())
596            namearg = _new(_ast.Name, '__data__', _ast.Load())
597            strarg = _new(_ast.Str, node.id)
598            node = _new(_ast.Call, name, [namearg, strarg], [])
599        elif isinstance(node.ctx, _ast.Store):
600            if len(self.locals) > 1:
601                self.locals[-1].add(node.id)
602
603        return node
604
605
606class ExpressionASTTransformer(TemplateASTTransformer):
607    """Concrete AST transformer that implements the AST transformations needed
608    for code embedded in templates.
609    """
610
611    def visit_Attribute(self, node):
612        if not isinstance(node.ctx, _ast.Load):
613            return ASTTransformer.visit_Attribute(self, node)
614
615        func = _new(_ast.Name, '_lookup_attr', _ast.Load())
616        args = [self.visit(node.value), _new(_ast.Str, node.attr)]
617        return _new(_ast.Call, func, args, [])
618
619    def visit_Subscript(self, node):
620        if not isinstance(node.ctx, _ast.Load) or \
621                not isinstance(node.slice, _ast.Index):
622            return ASTTransformer.visit_Subscript(self, node)
623
624        func = _new(_ast.Name, '_lookup_item', _ast.Load())
625        args = [
626            self.visit(node.value),
627            _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load())
628        ]
629        return _new(_ast.Call, func, args, [])
Note: See TracBrowser for help on using the repository browser.