From 5861585e94a32b3032ac473804bf90c6e1363940 Mon Sep 17 00:00:00 2001 From: Sebastian Silva Date: Wed, 28 Sep 2011 00:19:33 +0000 Subject: Migrated to Flask, added JQuery sugar theme, fixed race condition --- (limited to 'websdk/genshi/template/eval.py') diff --git a/websdk/genshi/template/eval.py b/websdk/genshi/template/eval.py new file mode 100644 index 0000000..8593aaa --- /dev/null +++ b/websdk/genshi/template/eval.py @@ -0,0 +1,629 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2010 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Support for "safe" evaluation of Python expressions.""" + +import __builtin__ + +from textwrap import dedent +from types import CodeType + +from genshi.core import Markup +from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, \ + _ast, parse +from genshi.template.base import TemplateRuntimeError +from genshi.util import flatten + +__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup', + 'Undefined', 'UndefinedError'] +__docformat__ = 'restructuredtext en' + + +# Check for a Python 2.4 bug in the eval loop +has_star_import_bug = False +try: + class _FakeMapping(object): + __getitem__ = __setitem__ = lambda *a: None + exec 'from sys import *' in {}, _FakeMapping() +except SystemError: + has_star_import_bug = True +del _FakeMapping + + +def _star_import_patch(mapping, modname): + """This function is used as helper if a Python version with a broken + star-import opcode is in use. + """ + module = __import__(modname, None, None, ['__all__']) + if hasattr(module, '__all__'): + members = module.__all__ + else: + members = [x for x in module.__dict__ if not x.startswith('_')] + mapping.update([(name, getattr(module, name)) for name in members]) + + +class Code(object): + """Abstract base class for the `Expression` and `Suite` classes.""" + __slots__ = ['source', 'code', 'ast', '_globals'] + + def __init__(self, source, filename=None, lineno=-1, lookup='strict', + xform=None): + """Create the code object, either from a string, or from an AST node. + + :param source: either a string containing the source code, or an AST + node + :param filename: the (preferably absolute) name of the file containing + the code + :param lineno: the number of the line on which the code was found + :param lookup: the lookup class that defines how variables are looked + up in the context; can be either "strict" (the default), + "lenient", or a custom lookup class + :param xform: the AST transformer that should be applied to the code; + if `None`, the appropriate transformation is chosen + depending on the mode + """ + if isinstance(source, basestring): + self.source = source + node = _parse(source, mode=self.mode) + else: + assert isinstance(source, _ast.AST), \ + 'Expected string or AST node, but got %r' % source + self.source = '?' + if self.mode == 'eval': + node = _ast.Expression() + node.body = source + else: + node = _ast.Module() + node.body = [source] + + self.ast = node + self.code = _compile(node, self.source, mode=self.mode, + filename=filename, lineno=lineno, xform=xform) + if lookup is None: + lookup = LenientLookup + elif isinstance(lookup, basestring): + lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup] + self._globals = lookup.globals + + def __getstate__(self): + state = {'source': self.source, 'ast': self.ast, + 'lookup': self._globals.im_self} + c = self.code + state['code'] = (c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code, + c.co_consts, c.co_names, c.co_varnames, c.co_filename, + c.co_name, c.co_firstlineno, c.co_lnotab, (), ()) + return state + + def __setstate__(self, state): + self.source = state['source'] + self.ast = state['ast'] + self.code = CodeType(0, *state['code']) + self._globals = state['lookup'].globals + + def __eq__(self, other): + return (type(other) == type(self)) and (self.code == other.code) + + def __hash__(self): + return hash(self.code) + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return '%s(%r)' % (type(self).__name__, self.source) + + +class Expression(Code): + """Evaluates Python expressions used in templates. + + >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) + >>> Expression('test').evaluate(data) + 'Foo' + + >>> Expression('items[0]').evaluate(data) + 1 + >>> Expression('items[-1]').evaluate(data) + 3 + >>> Expression('dict["some"]').evaluate(data) + 'thing' + + Similar to e.g. Javascript, expressions in templates can use the dot + notation for attribute access to access items in mappings: + + >>> Expression('dict.some').evaluate(data) + 'thing' + + This also works the other way around: item access can be used to access + any object attribute: + + >>> class MyClass(object): + ... myattr = 'Bar' + >>> data = dict(mine=MyClass(), key='myattr') + >>> Expression('mine.myattr').evaluate(data) + 'Bar' + >>> Expression('mine["myattr"]').evaluate(data) + 'Bar' + >>> Expression('mine[key]').evaluate(data) + 'Bar' + + All of the standard Python operators are available to template expressions. + Built-in functions such as ``len()`` are also available in template + expressions: + + >>> data = dict(items=[1, 2, 3]) + >>> Expression('len(items)').evaluate(data) + 3 + """ + __slots__ = [] + mode = 'eval' + + def evaluate(self, data): + """Evaluate the expression against the given data dictionary. + + :param data: a mapping containing the data to evaluate against + :return: the result of the evaluation + """ + __traceback_hide__ = 'before_and_this' + _globals = self._globals(data) + return eval(self.code, _globals, {'__data__': data}) + + +class Suite(Code): + """Executes Python statements used in templates. + + >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) + >>> Suite("foo = dict['some']").execute(data) + >>> data['foo'] + 'thing' + """ + __slots__ = [] + mode = 'exec' + + def execute(self, data): + """Execute the suite in the given data dictionary. + + :param data: a mapping containing the data to execute in + """ + __traceback_hide__ = 'before_and_this' + _globals = self._globals(data) + exec self.code in _globals, data + + +UNDEFINED = object() + + +class UndefinedError(TemplateRuntimeError): + """Exception thrown when a template expression attempts to access a variable + not defined in the context. + + :see: `LenientLookup`, `StrictLookup` + """ + def __init__(self, name, owner=UNDEFINED): + if owner is not UNDEFINED: + message = '%s has no member named "%s"' % (repr(owner), name) + else: + message = '"%s" not defined' % name + TemplateRuntimeError.__init__(self, message) + + +class Undefined(object): + """Represents a reference to an undefined variable. + + Unlike the Python runtime, template expressions can refer to an undefined + variable without causing a `NameError` to be raised. The result will be an + instance of the `Undefined` class, which is treated the same as ``False`` in + conditions, but raise an exception on any other operation: + + >>> foo = Undefined('foo') + >>> bool(foo) + False + >>> list(foo) + [] + >>> print(foo) + undefined + + However, calling an undefined variable, or trying to access an attribute + of that variable, will raise an exception that includes the name used to + reference that undefined variable. + + >>> foo('bar') + Traceback (most recent call last): + ... + UndefinedError: "foo" not defined + + >>> foo.bar + Traceback (most recent call last): + ... + UndefinedError: "foo" not defined + + :see: `LenientLookup` + """ + __slots__ = ['_name', '_owner'] + + def __init__(self, name, owner=UNDEFINED): + """Initialize the object. + + :param name: the name of the reference + :param owner: the owning object, if the variable is accessed as a member + """ + self._name = name + self._owner = owner + + def __iter__(self): + return iter([]) + + def __nonzero__(self): + return False + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self._name) + + def __str__(self): + return 'undefined' + + def _die(self, *args, **kwargs): + """Raise an `UndefinedError`.""" + __traceback_hide__ = True + raise UndefinedError(self._name, self._owner) + __call__ = __getattr__ = __getitem__ = _die + + # Hack around some behavior introduced in Python 2.6.2 + # http://genshi.edgewall.org/ticket/324 + __length_hint__ = None + + +class LookupBase(object): + """Abstract base class for variable lookup implementations.""" + + @classmethod + def globals(cls, data): + """Construct the globals dictionary to use as the execution context for + the expression or suite. + """ + return { + '__data__': data, + '_lookup_name': cls.lookup_name, + '_lookup_attr': cls.lookup_attr, + '_lookup_item': cls.lookup_item, + '_star_import_patch': _star_import_patch, + 'UndefinedError': UndefinedError, + } + + @classmethod + def lookup_name(cls, data, name): + __traceback_hide__ = True + val = data.get(name, UNDEFINED) + if val is UNDEFINED: + val = BUILTINS.get(name, val) + if val is UNDEFINED: + val = cls.undefined(name) + return val + + @classmethod + def lookup_attr(cls, obj, key): + __traceback_hide__ = True + try: + val = getattr(obj, key) + except AttributeError: + if hasattr(obj.__class__, key): + raise + else: + try: + val = obj[key] + except (KeyError, TypeError): + val = cls.undefined(key, owner=obj) + return val + + @classmethod + def lookup_item(cls, obj, key): + __traceback_hide__ = True + if len(key) == 1: + key = key[0] + try: + return obj[key] + except (AttributeError, KeyError, IndexError, TypeError), e: + if isinstance(key, basestring): + val = getattr(obj, key, UNDEFINED) + if val is UNDEFINED: + val = cls.undefined(key, owner=obj) + return val + raise + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Can be overridden by subclasses to specify behavior when undefined + variables are accessed. + + :param key: the name of the variable + :param owner: the owning object, if the variable is accessed as a member + """ + raise NotImplementedError + + +class LenientLookup(LookupBase): + """Default variable lookup mechanism for expressions. + + When an undefined variable is referenced using this lookup style, the + reference evaluates to an instance of the `Undefined` class: + + >>> expr = Expression('nothing', lookup='lenient') + >>> undef = expr.evaluate({}) + >>> undef + + + The same will happen when a non-existing attribute or item is accessed on + an existing object: + + >>> expr = Expression('something.nil', lookup='lenient') + >>> expr.evaluate({'something': dict()}) + + + See the documentation of the `Undefined` class for details on the behavior + of such objects. + + :see: `StrictLookup` + """ + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Return an ``Undefined`` object.""" + __traceback_hide__ = True + return Undefined(key, owner=owner) + + +class StrictLookup(LookupBase): + """Strict variable lookup mechanism for expressions. + + Referencing an undefined variable using this lookup style will immediately + raise an ``UndefinedError``: + + >>> expr = Expression('nothing', lookup='strict') + >>> expr.evaluate({}) + Traceback (most recent call last): + ... + UndefinedError: "nothing" not defined + + The same happens when a non-existing attribute or item is accessed on an + existing object: + + >>> expr = Expression('something.nil', lookup='strict') + >>> expr.evaluate({'something': dict()}) + Traceback (most recent call last): + ... + UndefinedError: {} has no member named "nil" + """ + + @classmethod + def undefined(cls, key, owner=UNDEFINED): + """Raise an ``UndefinedError`` immediately.""" + __traceback_hide__ = True + raise UndefinedError(key, owner=owner) + + +def _parse(source, mode='eval'): + source = source.strip() + if mode == 'exec': + lines = [line.expandtabs() for line in source.splitlines()] + if lines: + first = lines[0] + rest = dedent('\n'.join(lines[1:])).rstrip() + if first.rstrip().endswith(':') and not rest[0].isspace(): + rest = '\n'.join([' %s' % line for line in rest.splitlines()]) + source = '\n'.join([first, rest]) + if isinstance(source, unicode): + source = '\xef\xbb\xbf' + source.encode('utf-8') + return parse(source, mode) + + +def _compile(node, source=None, mode='eval', filename=None, lineno=-1, + xform=None): + if isinstance(filename, unicode): + # unicode file names not allowed for code objects + filename = filename.encode('utf-8', 'replace') + elif not filename: + filename = '' + if lineno <= 0: + lineno = 1 + + if xform is None: + xform = { + 'eval': ExpressionASTTransformer + }.get(mode, TemplateASTTransformer) + tree = xform().visit(node) + + if mode == 'eval': + name = '' % (source or '?') + else: + lines = source.splitlines() + if not lines: + extract = '' + else: + extract = lines[0] + if len(lines) > 1: + extract += ' ...' + name = '' % (extract) + new_source = ASTCodeGenerator(tree).code + code = compile(new_source, filename, mode) + + try: + # We'd like to just set co_firstlineno, but it's readonly. So we need + # to clone the code object while adjusting the line number + return CodeType(0, code.co_nlocals, code.co_stacksize, + code.co_flags | 0x0040, code.co_code, code.co_consts, + code.co_names, code.co_varnames, filename, name, + lineno, code.co_lnotab, (), ()) + except RuntimeError: + return code + + +def _new(class_, *args, **kwargs): + ret = class_() + for attr, value in zip(ret._fields, args): + if attr in kwargs: + raise ValueError('Field set both in args and kwargs') + setattr(ret, attr, value) + for attr, value in kwargs: + setattr(ret, attr, value) + return ret + + +BUILTINS = __builtin__.__dict__.copy() +BUILTINS.update({'Markup': Markup, 'Undefined': Undefined}) +CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis']) + + +class TemplateASTTransformer(ASTTransformer): + """Concrete AST transformer that implements the AST transformations needed + for code embedded in templates. + """ + + def __init__(self): + self.locals = [CONSTANTS] + + def _extract_names(self, node): + names = set() + def _process(node): + if isinstance(node, _ast.Name): + names.add(node.id) + elif isinstance(node, _ast.alias): + names.add(node.asname or node.name) + elif isinstance(node, _ast.Tuple): + for elt in node.elts: + _process(elt) + if hasattr(node, 'args'): + for arg in node.args: + _process(arg) + if hasattr(node, 'vararg'): + names.add(node.vararg) + if hasattr(node, 'kwarg'): + names.add(node.kwarg) + elif hasattr(node, 'names'): + for elt in node.names: + _process(elt) + return names + + def visit_Str(self, node): + if isinstance(node.s, str): + try: # If the string is ASCII, return a `str` object + node.s.decode('ascii') + except ValueError: # Otherwise return a `unicode` object + return _new(_ast.Str, node.s.decode('utf-8')) + return node + + def visit_ClassDef(self, node): + if len(self.locals) > 1: + self.locals[-1].add(node.name) + self.locals.append(set()) + try: + return ASTTransformer.visit_ClassDef(self, node) + finally: + self.locals.pop() + + def visit_Import(self, node): + if len(self.locals) > 1: + self.locals[-1].update(self._extract_names(node)) + return ASTTransformer.visit_Import(self, node) + + def visit_ImportFrom(self, node): + if [a.name for a in node.names] == ['*']: + if has_star_import_bug: + # This is a Python 2.4 bug. Only if we have a broken Python + # version do we need to apply this hack + node = _new(_ast.Expr, _new(_ast.Call, + _new(_ast.Name, '_star_import_patch'), [ + _new(_ast.Name, '__data__'), + _new(_ast.Str, node.module) + ], (), ())) + return node + if len(self.locals) > 1: + self.locals[-1].update(self._extract_names(node)) + return ASTTransformer.visit_ImportFrom(self, node) + + def visit_FunctionDef(self, node): + if len(self.locals) > 1: + self.locals[-1].add(node.name) + + self.locals.append(self._extract_names(node.args)) + try: + return ASTTransformer.visit_FunctionDef(self, node) + finally: + self.locals.pop() + + # GeneratorExp(expr elt, comprehension* generators) + def visit_GeneratorExp(self, node): + gens = [] + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self.locals.append(set()) + gen = _new(_ast.comprehension, self.visit(generator.target), + self.visit(generator.iter), + [self.visit(if_) for if_ in generator.ifs]) + gens.append(gen) + + # use node.__class__ to make it reusable as ListComp + ret = _new(node.__class__, self.visit(node.elt), gens) + #delete inserted locals + del self.locals[-len(node.generators):] + return ret + + # ListComp(expr elt, comprehension* generators) + visit_ListComp = visit_GeneratorExp + + def visit_Lambda(self, node): + self.locals.append(self._extract_names(node.args)) + try: + return ASTTransformer.visit_Lambda(self, node) + finally: + self.locals.pop() + + def visit_Name(self, node): + # If the name refers to a local inside a lambda, list comprehension, or + # generator expression, leave it alone + if isinstance(node.ctx, _ast.Load) and \ + node.id not in flatten(self.locals): + # Otherwise, translate the name ref into a context lookup + name = _new(_ast.Name, '_lookup_name', _ast.Load()) + namearg = _new(_ast.Name, '__data__', _ast.Load()) + strarg = _new(_ast.Str, node.id) + node = _new(_ast.Call, name, [namearg, strarg], []) + elif isinstance(node.ctx, _ast.Store): + if len(self.locals) > 1: + self.locals[-1].add(node.id) + + return node + + +class ExpressionASTTransformer(TemplateASTTransformer): + """Concrete AST transformer that implements the AST transformations needed + for code embedded in templates. + """ + + def visit_Attribute(self, node): + if not isinstance(node.ctx, _ast.Load): + return ASTTransformer.visit_Attribute(self, node) + + func = _new(_ast.Name, '_lookup_attr', _ast.Load()) + args = [self.visit(node.value), _new(_ast.Str, node.attr)] + return _new(_ast.Call, func, args, []) + + def visit_Subscript(self, node): + if not isinstance(node.ctx, _ast.Load) or \ + not isinstance(node.slice, _ast.Index): + return ASTTransformer.visit_Subscript(self, node) + + func = _new(_ast.Name, '_lookup_item', _ast.Load()) + args = [ + self.visit(node.value), + _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load()) + ] + return _new(_ast.Call, func, args, []) -- cgit v0.9.1