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@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@87: self.code = self._compile(source, filename, lineno) cmlenz@1: cmlenz@1: def __repr__(self): cmlenz@1: return '' % self.source cmlenz@1: cmlenz@81: def evaluate(self, data): 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@81: @return: the result of the evaluation cmlenz@81: """ cmlenz@90: retval = eval(self.code) cmlenz@90: if callable(retval): cmlenz@90: retval = retval() cmlenz@90: return retval cmlenz@30: cmlenz@87: def _compile(self, source, filename, lineno): cmlenz@87: tree = parse(self.source, 'eval') cmlenz@87: xform = ExpressionASTTransformer() cmlenz@87: tree = xform.visit(tree) cmlenz@30: cmlenz@87: if isinstance(filename, unicode): cmlenz@87: # pycodegen doesn't like unicode in the filename cmlenz@87: filename = filename.encode('utf-8', 'replace') cmlenz@87: tree.filename = filename or '' cmlenz@87: cmlenz@87: gen = ExpressionCodeGenerator(tree) cmlenz@87: if lineno >= 0: cmlenz@87: gen.emit('SET_LINENO', lineno) cmlenz@87: cmlenz@87: return gen.getCode() cmlenz@87: cmlenz@101: def _lookup_name(data, name, locals=None): cmlenz@87: val = data.get(name) cmlenz@88: if val is None and locals: cmlenz@88: val = locals.get(name) cmlenz@87: if val is None: cmlenz@87: val = getattr(__builtin__, name, None) cmlenz@87: return val cmlenz@101: _lookup_name = staticmethod(_lookup_name) cmlenz@87: cmlenz@101: def _lookup_attribute(data, obj, key): cmlenz@87: if hasattr(obj, key): cmlenz@87: return getattr(obj, key) cmlenz@87: try: cmlenz@87: return obj[key] cmlenz@87: except (KeyError, TypeError): cmlenz@87: return None cmlenz@101: _lookup_attribute = staticmethod(_lookup_attribute) cmlenz@87: cmlenz@101: def _lookup_item(data, obj, key): cmlenz@87: if len(key) == 1: cmlenz@87: key = key[0] cmlenz@87: try: cmlenz@87: return obj[key] cmlenz@87: except (KeyError, IndexError, TypeError), e: cmlenz@87: pass cmlenz@87: if isinstance(key, basestring): cmlenz@87: try: cmlenz@87: return getattr(obj, key) cmlenz@87: except (AttributeError, TypeError), e: cmlenz@87: pass cmlenz@101: _lookup_item = staticmethod(_lookup_item) cmlenz@87: 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@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 cmlenz@87: visitConst = visitKeyword = visitName = _visitDefault 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@87: """Concrete AST transformer that implementations the AST transformations cmlenz@87: needed for template expressions. cmlenz@87: """ cmlenz@87: cmlenz@88: def visitGetattr(self, node, *args, **kwargs): cmlenz@87: return ast.CallFunc( cmlenz@87: ast.Getattr(ast.Name('self'), '_lookup_attribute'), cmlenz@88: [ast.Name('data'), self.visit(node.expr, *args, **kwargs), cmlenz@88: ast.Const(node.attrname)] cmlenz@87: ) cmlenz@30: cmlenz@88: def visitListComp(self, node, *args, **kwargs): cmlenz@88: old_lookup_locals = kwargs.get('lookup_locals', False) cmlenz@88: kwargs['lookup_locals'] = True 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: kwargs['lookup_locals'] = old_lookup_locals cmlenz@88: return node cmlenz@88: cmlenz@88: def visitName(self, node, *args, **kwargs): cmlenz@88: func_args = [ast.Name('data'), ast.Const(node.name)] cmlenz@88: if kwargs.get('lookup_locals'): cmlenz@88: func_args.append(ast.CallFunc(ast.Name('locals'), [])) cmlenz@87: return ast.CallFunc( cmlenz@88: ast.Getattr(ast.Name('self'), '_lookup_name'), func_args cmlenz@87: ) cmlenz@87: return node cmlenz@81: cmlenz@88: def visitSubscript(self, node, *args, **kwargs): cmlenz@87: return ast.CallFunc( cmlenz@87: ast.Getattr(ast.Name('self'), '_lookup_item'), cmlenz@88: [ast.Name('data'), self.visit(node.expr, *args, **kwargs), cmlenz@88: ast.Tuple(map(self.visit, node.subs, *args, **kwargs))] cmlenz@87: )