cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # cmlenz@66: # Copyright (C) 2006 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@66: # are also available at http://markup.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@66: # history and logs, available at http://markup.edgewall.org/log/. cmlenz@27: cmlenz@27: """Support for "safe" evaluation of Python expressions.""" cmlenz@1: cmlenz@1: import __builtin__ cmlenz@87: from compiler import ast, parse cmlenz@87: from compiler.pycodegen import ExpressionCodeGenerator cmlenz@131: import new cmlenz@1: cmlenz@1: from markup.core import Stream cmlenz@1: cmlenz@1: __all__ = ['Expression'] cmlenz@1: cmlenz@1: cmlenz@1: class Expression(object): cmlenz@1: """Evaluates Python expressions used in templates. cmlenz@1: cmlenz@1: >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) cmlenz@1: >>> Expression('test').evaluate(data) cmlenz@1: 'Foo' cmlenz@81: cmlenz@1: >>> Expression('items[0]').evaluate(data) cmlenz@1: 1 cmlenz@1: >>> Expression('items[-1]').evaluate(data) cmlenz@1: 3 cmlenz@1: >>> Expression('dict["some"]').evaluate(data) cmlenz@1: 'thing' cmlenz@1: cmlenz@1: Similar to e.g. Javascript, expressions in templates can use the dot cmlenz@1: notation for attribute access to access items in mappings: cmlenz@1: cmlenz@1: >>> Expression('dict.some').evaluate(data) cmlenz@1: 'thing' cmlenz@86: cmlenz@1: This also works the other way around: item access can be used to access cmlenz@1: any object attribute (meaning there's no use for `getattr()` in templates): cmlenz@1: cmlenz@1: >>> class MyClass(object): cmlenz@1: ... myattr = 'Bar' cmlenz@1: >>> data = dict(mine=MyClass(), key='myattr') cmlenz@1: >>> Expression('mine.myattr').evaluate(data) cmlenz@1: 'Bar' cmlenz@1: >>> Expression('mine["myattr"]').evaluate(data) cmlenz@1: 'Bar' cmlenz@1: >>> Expression('mine[key]').evaluate(data) cmlenz@1: 'Bar' cmlenz@1: cmlenz@31: All of the standard Python operators are available to template expressions. cmlenz@1: Built-in functions such as `len()` are also available in template cmlenz@1: expressions: cmlenz@1: cmlenz@1: >>> data = dict(items=[1, 2, 3]) cmlenz@1: >>> Expression('len(items)').evaluate(data) cmlenz@1: 3 cmlenz@1: """ cmlenz@81: __slots__ = ['source', 'code'] cmlenz@1: cmlenz@81: def __init__(self, source, filename=None, lineno=-1): cmlenz@27: """Create the expression. cmlenz@27: cmlenz@27: @param source: the expression as string cmlenz@27: """ cmlenz@1: self.source = source cmlenz@134: self.code = _compile(self, filename, lineno) cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: return '' % self.source cmlenz@1: cmlenz@120: def evaluate(self, data, nocall=False): cmlenz@81: """Evaluate the expression against the given data dictionary. cmlenz@81: cmlenz@81: @param data: a mapping containing the data to evaluate against cmlenz@120: @param nocall: if true, the result of the evaluation is not called if cmlenz@120: if it is a callable cmlenz@81: @return: the result of the evaluation cmlenz@81: """ cmlenz@118: retval = eval(self.code, {'data': data, cmlenz@118: '_lookup_name': _lookup_name, cmlenz@118: '_lookup_attr': _lookup_attr, cmlenz@118: '_lookup_item': _lookup_item}) cmlenz@120: if not nocall and callable(retval): cmlenz@90: retval = retval() cmlenz@90: return retval cmlenz@30: cmlenz@87: cmlenz@134: def _compile(expr, filename=None, lineno=-1): cmlenz@134: tree = parse(expr.source, 'eval') cmlenz@116: xform = ExpressionASTTransformer() cmlenz@116: tree = xform.visit(tree) cmlenz@87: cmlenz@116: if isinstance(filename, unicode): cmlenz@131: # unicode file names not allowed for code objects cmlenz@116: filename = filename.encode('utf-8', 'replace') cmlenz@131: elif not filename: cmlenz@131: filename = '' cmlenz@132: tree.filename = filename cmlenz@131: if lineno <= 0: cmlenz@131: lineno = 1 cmlenz@87: cmlenz@116: gen = ExpressionCodeGenerator(tree) cmlenz@131: gen.optimized = True cmlenz@131: code = gen.getCode() cmlenz@87: cmlenz@131: # We'd like to just set co_firstlineno, but it's readonly. So we need to cmlenz@131: # clone the code object while adjusting the line number cmlenz@131: return new.code(0, code.co_nlocals, code.co_stacksize, cmlenz@131: code.co_flags | 0x0040, code.co_code, code.co_consts, cmlenz@134: code.co_names, code.co_varnames, filename, repr(expr), cmlenz@131: lineno, code.co_lnotab, (), ()) cmlenz@116: cmlenz@132: def _lookup_name(data, name, locals_=None): cmlenz@132: val = None cmlenz@132: if locals_: cmlenz@132: val = locals_.get(name) cmlenz@132: if val is None: cmlenz@132: val = data.get(name) cmlenz@116: if val is None: cmlenz@116: val = getattr(__builtin__, name, None) cmlenz@116: return val cmlenz@116: cmlenz@118: def _lookup_attr(data, obj, key): cmlenz@116: if hasattr(obj, key): cmlenz@116: return getattr(obj, key) cmlenz@116: try: cmlenz@116: return obj[key] cmlenz@116: except (KeyError, TypeError): cmlenz@116: return None cmlenz@116: cmlenz@116: def _lookup_item(data, obj, key): cmlenz@116: if len(key) == 1: cmlenz@116: key = key[0] cmlenz@116: try: cmlenz@116: return obj[key] cmlenz@116: except (KeyError, IndexError, TypeError), e: cmlenz@116: if isinstance(key, basestring): cmlenz@116: try: cmlenz@116: return getattr(obj, key) cmlenz@116: except (AttributeError, TypeError), e: cmlenz@116: pass cmlenz@87: cmlenz@87: class ASTTransformer(object): cmlenz@87: """General purpose base class for AST transformations. cmlenz@87: cmlenz@87: Every visitor method can be overridden to return an AST node that has been cmlenz@87: altered or replaced in some way. cmlenz@87: """ cmlenz@87: _visitors = {} cmlenz@87: cmlenz@88: def visit(self, node, *args, **kwargs): cmlenz@87: v = self._visitors.get(node.__class__) cmlenz@87: if not v: cmlenz@87: v = getattr(self, 'visit%s' % node.__class__.__name__) cmlenz@87: self._visitors[node.__class__] = v cmlenz@88: return v(node, *args, **kwargs) cmlenz@87: cmlenz@88: def visitExpression(self, node, *args, **kwargs): cmlenz@88: node.node = self.visit(node.node, *args, **kwargs) cmlenz@87: return node cmlenz@87: cmlenz@87: # Functions & Accessors cmlenz@87: cmlenz@88: def visitCallFunc(self, node, *args, **kwargs): cmlenz@88: node.node = self.visit(node.node, *args, **kwargs) cmlenz@88: node.args = map(lambda x: self.visit(x, *args, **kwargs), node.args) cmlenz@87: if node.star_args: cmlenz@88: node.star_args = map(lambda x: self.visit(x, *args, **kwargs), cmlenz@88: node.star_args) cmlenz@87: if node.dstar_args: cmlenz@88: node.dstart_args = map(lambda x: self.visit(x, *args, **kwargs), cmlenz@88: node.dstar_args) cmlenz@87: return node cmlenz@30: cmlenz@118: def visitLambda(self, node, *args, **kwargs): cmlenz@118: node.code = self.visit(node.code, *args, **kwargs) cmlenz@118: node.filename = '' # workaround for bug in pycodegen cmlenz@118: return node cmlenz@118: cmlenz@88: def visitGetattr(self, node, *args, **kwargs): cmlenz@88: node.expr = self.visit(node.expr, *args, **kwargs) cmlenz@87: return node cmlenz@30: cmlenz@88: def visitSubscript(self, node, *args, **kwargs): cmlenz@88: node.expr = self.visit(node.expr, *args, **kwargs) cmlenz@88: node.subs = map(lambda x: self.visit(x, *args, **kwargs), node.subs) cmlenz@87: return node cmlenz@30: cmlenz@87: # Operators cmlenz@87: cmlenz@88: def _visitBoolOp(self, node, *args, **kwargs): cmlenz@88: node.nodes = map(lambda x: self.visit(x, *args, **kwargs), node.nodes) cmlenz@87: return node cmlenz@87: visitAnd = visitOr = visitBitand = visitBitor = _visitBoolOp cmlenz@87: cmlenz@88: def _visitBinOp(self, node, *args, **kwargs): cmlenz@88: node.left = self.visit(node.left, *args, **kwargs) cmlenz@88: node.right = self.visit(node.right, *args, **kwargs) cmlenz@87: return node cmlenz@87: visitAdd = visitSub = _visitBinOp cmlenz@87: visitDiv = visitFloorDiv = visitMod = visitMul = visitPower = _visitBinOp cmlenz@87: visitLeftShift = visitRightShift = _visitBinOp cmlenz@87: cmlenz@88: def visitCompare(self, node, *args, **kwargs): cmlenz@88: node.expr = self.visit(node.expr, *args, **kwargs) cmlenz@88: node.ops = map(lambda (op, n): (op, self.visit(n, *args, **kwargs)), cmlenz@87: node.ops) cmlenz@87: return node cmlenz@87: cmlenz@88: def _visitUnaryOp(self, node, *args, **kwargs): cmlenz@88: node.expr = self.visit(node.expr, *args, **kwargs) cmlenz@87: return node cmlenz@87: visitUnaryAdd = visitUnarySub = visitNot = visitInvert = _visitUnaryOp cmlenz@98: visitBackquote = _visitUnaryOp cmlenz@87: cmlenz@88: # Identifiers, Literals and Comprehensions cmlenz@87: cmlenz@88: def _visitDefault(self, node, *args, **kwargs): cmlenz@87: return node cmlenz@88: visitAssName = visitAssTuple = _visitDefault jonas@102: visitConst = visitName = _visitDefault jonas@102: jonas@102: def visitKeyword(self, node, *args, **kwargs): jonas@102: node.expr = self.visit(node.expr, *args, **kwargs) jonas@102: return node cmlenz@87: cmlenz@88: def visitDict(self, node, *args, **kwargs): cmlenz@88: node.items = map(lambda (k, v): (self.visit(k, *args, **kwargs), cmlenz@88: self.visit(v, *args, **kwargs)), cmlenz@87: node.items) cmlenz@87: return node cmlenz@87: cmlenz@88: def visitTuple(self, node, *args, **kwargs): cmlenz@88: node.nodes = map(lambda n: self.visit(n, *args, **kwargs), node.nodes) cmlenz@87: return node cmlenz@87: cmlenz@88: def visitList(self, node, *args, **kwargs): cmlenz@88: node.nodes = map(lambda n: self.visit(n, *args, **kwargs), node.nodes) cmlenz@88: return node cmlenz@88: cmlenz@88: def visitListComp(self, node, *args, **kwargs): cmlenz@88: node.expr = self.visit(node.expr, *args, **kwargs) cmlenz@88: node.quals = map(lambda x: self.visit(x, *args, **kwargs), node.quals) cmlenz@88: return node cmlenz@88: cmlenz@88: def visitListCompFor(self, node, *args, **kwargs): cmlenz@88: node.assign = self.visit(node.assign, *args, **kwargs) cmlenz@88: node.list = self.visit(node.list, *args, **kwargs) cmlenz@88: node.ifs = map(lambda x: self.visit(x, *args, **kwargs), node.ifs) cmlenz@88: return node cmlenz@88: cmlenz@88: def visitListCompIf(self, node, *args, **kwargs): cmlenz@88: node.test = self.visit(node.test, *args, **kwargs) cmlenz@87: return node cmlenz@87: cmlenz@87: cmlenz@87: class ExpressionASTTransformer(ASTTransformer): cmlenz@112: """Concrete AST transformer that implements the AST transformations needed cmlenz@112: for template expressions. cmlenz@87: """ cmlenz@87: cmlenz@131: def visitGetattr(self, node, locals_=False): cmlenz@131: return ast.CallFunc(ast.Name('_lookup_attr'), [ cmlenz@131: ast.Name('data'), self.visit(node.expr, locals_=locals_), cmlenz@131: ast.Const(node.attrname) cmlenz@131: ]) cmlenz@30: cmlenz@131: def visitLambda(self, node, locals_=False): cmlenz@131: node.code = self.visit(node.code, locals_=True) cmlenz@118: node.filename = '' # workaround for bug in pycodegen cmlenz@118: return node cmlenz@118: cmlenz@131: def visitListComp(self, node, locals_=False): cmlenz@131: node.expr = self.visit(node.expr, locals_=True) cmlenz@131: node.quals = map(lambda x: self.visit(x, locals_=True), node.quals) cmlenz@88: return node cmlenz@88: cmlenz@131: def visitName(self, node, locals_=False): cmlenz@88: func_args = [ast.Name('data'), ast.Const(node.name)] cmlenz@131: if locals_: cmlenz@88: func_args.append(ast.CallFunc(ast.Name('locals'), [])) cmlenz@116: return ast.CallFunc(ast.Name('_lookup_name'), func_args) cmlenz@81: cmlenz@131: def visitSubscript(self, node, locals_=False): cmlenz@131: return ast.CallFunc(ast.Name('_lookup_item'), [ cmlenz@131: ast.Name('data'), self.visit(node.expr, locals_=locals_), cmlenz@131: ast.Tuple(map(lambda x: self.visit(x, locals_=locals_), node.subs)) cmlenz@131: ])