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
27from genshi.compat import get_code_params, build_code_chunk, isstring, \
28                          IS_PYTHON2, _ast_Str
29
30__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
31           'Undefined', 'UndefinedError']
32__docformat__ = 'restructuredtext en'
33
34
35# Check for a Python 2.4 bug in the eval loop
36has_star_import_bug = False
37try:
38    class _FakeMapping(object):
39        __getitem__ = __setitem__ = lambda *a: None
40    exec 'from sys import *' in {}, _FakeMapping()
41except SystemError:
42    has_star_import_bug = True
43del _FakeMapping
44
45
46def _star_import_patch(mapping, modname):
47    """This function is used as helper if a Python version with a broken
48    star-import opcode is in use.
49    """
50    module = __import__(modname, None, None, ['__all__'])
51    if hasattr(module, '__all__'):
52        members = module.__all__
53    else:
54        members = [x for x in module.__dict__ if not x.startswith('_')]
55    mapping.update([(name, getattr(module, name)) for name in members])
56
57
58class Code(object):
59    """Abstract base class for the `Expression` and `Suite` classes."""
60    __slots__ = ['source', 'code', 'ast', '_globals']
61
62    def __init__(self, source, filename=None, lineno=-1, lookup='strict',
63                 xform=None):
64        """Create the code object, either from a string, or from an AST node.
65
66        :param source: either a string containing the source code, or an AST
67                       node
68        :param filename: the (preferably absolute) name of the file containing
69                         the code
70        :param lineno: the number of the line on which the code was found
71        :param lookup: the lookup class that defines how variables are looked
72                       up in the context; can be either "strict" (the default),
73                       "lenient", or a custom lookup class
74        :param xform: the AST transformer that should be applied to the code;
75                      if `None`, the appropriate transformation is chosen
76                      depending on the mode
77        """
78        if isinstance(source, basestring):
79            self.source = source
80            node = _parse(source, mode=self.mode)
81        else:
82            assert isinstance(source, _ast.AST), \
83                'Expected string or AST node, but got %r' % source
84            self.source = '?'
85            if self.mode == 'eval':
86                node = _ast.Expression()
87                node.body = source
88            else:
89                node = _ast.Module()
90                node.body = [source]
91
92        self.ast = node
93        self.code = _compile(node, self.source, mode=self.mode,
94                             filename=filename, lineno=lineno, xform=xform)
95        if lookup is None:
96            lookup = LenientLookup
97        elif isinstance(lookup, basestring):
98            lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
99        self._globals = lookup.globals
100
101    def __getstate__(self):
102        state = {'source': self.source, 'ast': self.ast,
103                 'lookup': self._globals.im_self}
104        state['code'] = get_code_params(self.code)
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    >>> try:
240    ...     foo('bar')
241    ... except UndefinedError, e:
242    ...     print e.msg
243    "foo" not defined
244
245    >>> try:
246    ...     foo.bar
247    ... except UndefinedError, e:
248    ...     print e.msg
249    "foo" not defined
250
251    :see: `LenientLookup`
252    """
253    __slots__ = ['_name', '_owner']
254
255    def __init__(self, name, owner=UNDEFINED):
256        """Initialize the object.
257
258        :param name: the name of the reference
259        :param owner: the owning object, if the variable is accessed as a member
260        """
261        self._name = name
262        self._owner = owner
263
264    def __iter__(self):
265        return iter([])
266
267    def __nonzero__(self):
268        return False
269
270    def __repr__(self):
271        return '<%s %r>' % (type(self).__name__, self._name)
272
273    def __str__(self):
274        return 'undefined'
275
276    def _die(self, *args, **kwargs):
277        """Raise an `UndefinedError`."""
278        __traceback_hide__ = True
279        raise UndefinedError(self._name, self._owner)
280    __call__ = __getattr__ = __getitem__ = _die
281
282    # Hack around some behavior introduced in Python 2.6.2
283    # http://genshi.edgewall.org/ticket/324
284    __length_hint__ = None
285
286
287class LookupBase(object):
288    """Abstract base class for variable lookup implementations."""
289
290    @classmethod
291    def globals(cls, data):
292        """Construct the globals dictionary to use as the execution context for
293        the expression or suite.
294        """
295        return {
296            '__data__': data,
297            '_lookup_name': cls.lookup_name,
298            '_lookup_attr': cls.lookup_attr,
299            '_lookup_item': cls.lookup_item,
300            '_star_import_patch': _star_import_patch,
301            'UndefinedError': UndefinedError,
302        }
303
304    @classmethod
305    def lookup_name(cls, data, name):
306        __traceback_hide__ = True
307        val = data.get(name, UNDEFINED)
308        if val is UNDEFINED:
309            val = BUILTINS.get(name, val)
310            if val is UNDEFINED:
311                val = cls.undefined(name)
312        return val
313
314    @classmethod
315    def lookup_attr(cls, obj, key):
316        __traceback_hide__ = True
317        try:
318            val = getattr(obj, key)
319        except AttributeError:
320            if hasattr(obj.__class__, key):
321                raise
322            else:
323                try:
324                    val = obj[key]
325                except (KeyError, TypeError):
326                    val = cls.undefined(key, owner=obj)
327        return val
328
329    @classmethod
330    def lookup_item(cls, obj, key):
331        __traceback_hide__ = True
332        if len(key) == 1:
333            key = key[0]
334        try:
335            return obj[key]
336        except (AttributeError, KeyError, IndexError, TypeError), e:
337            if isinstance(key, basestring):
338                val = getattr(obj, key, UNDEFINED)
339                if val is UNDEFINED:
340                    val = cls.undefined(key, owner=obj)
341                return val
342            raise
343
344    @classmethod
345    def undefined(cls, key, owner=UNDEFINED):
346        """Can be overridden by subclasses to specify behavior when undefined
347        variables are accessed.
348
349        :param key: the name of the variable
350        :param owner: the owning object, if the variable is accessed as a member
351        """
352        raise NotImplementedError
353
354
355class LenientLookup(LookupBase):
356    """Default variable lookup mechanism for expressions.
357
358    When an undefined variable is referenced using this lookup style, the
359    reference evaluates to an instance of the `Undefined` class:
360
361    >>> expr = Expression('nothing', lookup='lenient')
362    >>> undef = expr.evaluate({})
363    >>> undef
364    <Undefined 'nothing'>
365
366    The same will happen when a non-existing attribute or item is accessed on
367    an existing object:
368
369    >>> expr = Expression('something.nil', lookup='lenient')
370    >>> expr.evaluate({'something': dict()})
371    <Undefined 'nil'>
372
373    See the documentation of the `Undefined` class for details on the behavior
374    of such objects.
375
376    :see: `StrictLookup`
377    """
378
379    @classmethod
380    def undefined(cls, key, owner=UNDEFINED):
381        """Return an ``Undefined`` object."""
382        __traceback_hide__ = True
383        return Undefined(key, owner=owner)
384
385
386class StrictLookup(LookupBase):
387    """Strict variable lookup mechanism for expressions.
388
389    Referencing an undefined variable using this lookup style will immediately
390    raise an ``UndefinedError``:
391
392    >>> expr = Expression('nothing', lookup='strict')
393    >>> try:
394    ...     expr.evaluate({})
395    ... except UndefinedError, e:
396    ...     print e.msg
397    "nothing" not defined
398
399    The same happens when a non-existing attribute or item is accessed on an
400    existing object:
401
402    >>> expr = Expression('something.nil', lookup='strict')
403    >>> try:
404    ...     expr.evaluate({'something': dict()})
405    ... except UndefinedError, e:
406    ...     print e.msg
407    {} has no member named "nil"
408    """
409
410    @classmethod
411    def undefined(cls, key, owner=UNDEFINED):
412        """Raise an ``UndefinedError`` immediately."""
413        __traceback_hide__ = True
414        raise UndefinedError(key, owner=owner)
415
416
417def _parse(source, mode='eval'):
418    source = source.strip()
419    if mode == 'exec':
420        lines = [line.expandtabs() for line in source.splitlines()]
421        if lines:
422            first = lines[0]
423            rest = dedent('\n'.join(lines[1:])).rstrip()
424            if first.rstrip().endswith(':') and not rest[0].isspace():
425                rest = '\n'.join(['    %s' % line for line in rest.splitlines()])
426            source = '\n'.join([first, rest])
427    if isinstance(source, unicode):
428        source = (u'\ufeff' + source).encode('utf-8')
429    return parse(source, mode)
430
431
432def _compile(node, source=None, mode='eval', filename=None, lineno=-1,
433             xform=None):
434    if not filename:
435        filename = '<string>'
436    if IS_PYTHON2:
437        # Python 2 requires non-unicode filenames
438        if isinstance(filename, unicode):
439            filename = filename.encode('utf-8', 'replace')
440    else:
441        # Python 3 requires unicode filenames
442        if not isinstance(filename, unicode):
443            filename = filename.decode('utf-8', 'replace')
444    if lineno <= 0:
445        lineno = 1
446
447    if xform is None:
448        xform = {
449            'eval': ExpressionASTTransformer
450        }.get(mode, TemplateASTTransformer)
451    tree = xform().visit(node)
452
453    if mode == 'eval':
454        name = '<Expression %r>' % (source or '?')
455    else:
456        lines = source.splitlines()
457        if not lines:
458            extract = ''
459        else:
460            extract = lines[0]
461        if len(lines) > 1:
462            extract += ' ...'
463        name = '<Suite %r>' % (extract)
464    new_source = ASTCodeGenerator(tree).code
465    code = compile(new_source, filename, mode)
466
467    try:
468        # We'd like to just set co_firstlineno, but it's readonly. So we need
469        # to clone the code object while adjusting the line number
470        return build_code_chunk(code, filename, name, lineno)
471    except RuntimeError:
472        return code
473
474
475def _new(class_, *args, **kwargs):
476    ret = class_()
477    for attr, value in zip(ret._fields, args):
478        if attr in kwargs:
479            raise ValueError('Field set both in args and kwargs')
480        setattr(ret, attr, value)
481    for attr, value in kwargs:
482        setattr(ret, attr, value)
483    return ret
484
485
486BUILTINS = __builtin__.__dict__.copy()
487BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
488CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
489
490
491class TemplateASTTransformer(ASTTransformer):
492    """Concrete AST transformer that implements the AST transformations needed
493    for code embedded in templates.
494    """
495
496    def __init__(self):
497        self.locals = [CONSTANTS]
498
499    def _process(self, names, node):
500        if not IS_PYTHON2 and isinstance(node, _ast.arg):
501            names.add(node.arg)
502        elif isstring(node):
503            names.add(node)
504        elif isinstance(node, _ast.Name):
505            names.add(node.id)
506        elif isinstance(node, _ast.alias):
507            names.add(node.asname or node.name)
508        elif isinstance(node, _ast.Tuple):
509            for elt in node.elts:
510                self._process(names, elt)
511
512    def _extract_names(self, node):
513        names = set()
514        if hasattr(node, 'args'):
515            for arg in node.args:
516                self._process(names, arg)
517            if hasattr(node, 'kwonlyargs'):
518                for arg in node.kwonlyargs:
519                    self._process(names, arg)
520            if hasattr(node, 'vararg'):
521                self._process(names, node.vararg)
522            if hasattr(node, 'kwarg'):
523                self._process(names, node.kwarg)
524        elif hasattr(node, 'names'):
525            for elt in node.names:
526                self._process(names, elt)
527        return names
528
529    def visit_Str(self, node):
530        if not isinstance(node.s, unicode):
531            try: # If the string is ASCII, return a `str` object
532                node.s.decode('ascii')
533            except ValueError: # Otherwise return a `unicode` object
534                return _new(_ast_Str, node.s.decode('utf-8'))
535        return node
536
537    def visit_ClassDef(self, node):
538        if len(self.locals) > 1:
539            self.locals[-1].add(node.name)
540        self.locals.append(set())
541        try:
542            return ASTTransformer.visit_ClassDef(self, node)
543        finally:
544            self.locals.pop()
545
546    def visit_Import(self, node):
547        if len(self.locals) > 1:
548            self.locals[-1].update(self._extract_names(node))
549        return ASTTransformer.visit_Import(self, node)
550
551    def visit_ImportFrom(self, node):
552        if [a.name for a in node.names] == ['*']:
553            if has_star_import_bug:
554                # This is a Python 2.4 bug. Only if we have a broken Python
555                # version do we need to apply this hack
556                node = _new(_ast.Expr, _new(_ast.Call,
557                    _new(_ast.Name, '_star_import_patch'), [
558                        _new(_ast.Name, '__data__'),
559                        _new(_ast_Str, node.module)
560                    ], (), ()))
561            return node
562        if len(self.locals) > 1:
563            self.locals[-1].update(self._extract_names(node))
564        return ASTTransformer.visit_ImportFrom(self, node)
565
566    def visit_FunctionDef(self, node):
567        if len(self.locals) > 1:
568            self.locals[-1].add(node.name)
569
570        self.locals.append(self._extract_names(node.args))
571        try:
572            return ASTTransformer.visit_FunctionDef(self, node)
573        finally:
574            self.locals.pop()
575
576    # GeneratorExp(expr elt, comprehension* generators)
577    def visit_GeneratorExp(self, node):
578        gens = []
579        for generator in node.generators:
580            # comprehension = (expr target, expr iter, expr* ifs)
581            self.locals.append(set())
582            gen = _new(_ast.comprehension, self.visit(generator.target),
583                       self.visit(generator.iter),
584                       [self.visit(if_) for if_ in generator.ifs])
585            gens.append(gen)
586
587        # use node.__class__ to make it reusable as ListComp
588        ret = _new(node.__class__, self.visit(node.elt), gens)
589        #delete inserted locals
590        del self.locals[-len(node.generators):]
591        return ret
592
593    # ListComp(expr elt, comprehension* generators)
594    visit_ListComp = visit_GeneratorExp
595
596    def visit_Lambda(self, node):
597        self.locals.append(self._extract_names(node.args))
598        try:
599            return ASTTransformer.visit_Lambda(self, node)
600        finally:
601            self.locals.pop()
602
603    # Only used in Python 3.5+
604    def visit_Starred(self, node):
605        node.value = self.visit(node.value)
606        return node
607
608    def visit_Name(self, node):
609        # If the name refers to a local inside a lambda, list comprehension, or
610        # generator expression, leave it alone
611        if isinstance(node.ctx, _ast.Load) and \
612                node.id not in flatten(self.locals):
613            # Otherwise, translate the name ref into a context lookup
614            name = _new(_ast.Name, '_lookup_name', _ast.Load())
615            namearg = _new(_ast.Name, '__data__', _ast.Load())
616            strarg = _new(_ast_Str, node.id)
617            node = _new(_ast.Call, name, [namearg, strarg], [])
618        elif isinstance(node.ctx, _ast.Store):
619            if len(self.locals) > 1:
620                self.locals[-1].add(node.id)
621
622        return node
623
624
625class ExpressionASTTransformer(TemplateASTTransformer):
626    """Concrete AST transformer that implements the AST transformations needed
627    for code embedded in templates.
628    """
629
630    def visit_Attribute(self, node):
631        if not isinstance(node.ctx, _ast.Load):
632            return ASTTransformer.visit_Attribute(self, node)
633
634        func = _new(_ast.Name, '_lookup_attr', _ast.Load())
635        args = [self.visit(node.value), _new(_ast_Str, node.attr)]
636        return _new(_ast.Call, func, args, [])
637
638    def visit_Subscript(self, node):
639        if not isinstance(node.ctx, _ast.Load) or \
640                not isinstance(node.slice, _ast.Index):
641            return ASTTransformer.visit_Subscript(self, node)
642
643        func = _new(_ast.Name, '_lookup_item', _ast.Load())
644        args = [
645            self.visit(node.value),
646            _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load())
647        ]
648        return _new(_ast.Call, func, args, [])
649