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@32: from __future__ import division cmlenz@32: cmlenz@1: import __builtin__ cmlenz@81: from compiler import parse, pycodegen 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@81: """ cmlenz@81: """ 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@69: _visitors = {} 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@81: cmlenz@81: tree = parse(self.source, 'eval') cmlenz@81: if isinstance(filename, unicode): cmlenz@81: # pycodegen doesn't like unicode in the filename cmlenz@81: filename = filename.encode('utf-8', 'replace') cmlenz@81: tree.filename = filename or '' cmlenz@81: gen = TemplateExpressionCodeGenerator(tree) cmlenz@81: if lineno >= 0: cmlenz@81: gen.emit('SET_LINENO', lineno) cmlenz@81: self.code = gen.getCode() 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@81: return eval(self.code) cmlenz@30: cmlenz@30: cmlenz@81: class TemplateExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator): cmlenz@30: cmlenz@81: def visitGetattr(self, node): cmlenz@81: """Overridden to fallback to item access if the object doesn't have an cmlenz@81: attribute. cmlenz@81: cmlenz@81: Also, if either method fails, this returns `None` instead of raising an cmlenz@81: `AttributeError`. cmlenz@81: """ cmlenz@81: # check whether the object has the request attribute cmlenz@81: self.visit(node.expr) cmlenz@81: self.emit('STORE_NAME', 'obj') cmlenz@81: self.emit('LOAD_GLOBAL', 'hasattr') cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: self.emit('LOAD_CONST', node.attrname) cmlenz@81: self.emit('CALL_FUNCTION', 2) cmlenz@81: else_ = self.newBlock() cmlenz@81: self.emit('JUMP_IF_FALSE', else_) cmlenz@81: self.emit('POP_TOP') cmlenz@30: cmlenz@81: # hasattr returned True, so return the attribute value cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: self.emit('LOAD_ATTR', node.attrname) cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: return_ = self.newBlock() cmlenz@81: self.emit('JUMP_FORWARD', return_) cmlenz@30: cmlenz@81: # hasattr returned False, so try item access cmlenz@81: self.startBlock(else_) cmlenz@81: try_ = self.newBlock() cmlenz@81: except_ = self.newBlock() cmlenz@81: self.emit('SETUP_EXCEPT', except_) cmlenz@81: self.nextBlock(try_) cmlenz@81: self.setups.push((pycodegen.EXCEPT, try_)) cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: self.emit('LOAD_CONST', node.attrname) cmlenz@81: self.emit('BINARY_SUBSCR') cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: self.emit('POP_BLOCK') cmlenz@81: self.setups.pop() cmlenz@81: self.emit('JUMP_FORWARD', return_) cmlenz@30: cmlenz@81: # exception handler: just return `None` cmlenz@81: self.startBlock(except_) cmlenz@81: self.emit('DUP_TOP') cmlenz@81: self.emit('LOAD_GLOBAL', 'KeyError') cmlenz@81: self.emit('LOAD_GLOBAL', 'TypeError') cmlenz@81: self.emit('BUILD_TUPLE', 2) cmlenz@81: self.emit('COMPARE_OP', 'exception match') cmlenz@81: next = self.newBlock() cmlenz@81: self.emit('JUMP_IF_FALSE', next) cmlenz@81: self.nextBlock() cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('LOAD_CONST', None) # exception handler body cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: self.emit('JUMP_FORWARD', return_) cmlenz@81: self.nextBlock(next) cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('END_FINALLY') cmlenz@81: cmlenz@81: # return cmlenz@81: self.nextBlock(return_) cmlenz@81: self.emit('LOAD_NAME', 'val') cmlenz@30: cmlenz@81: def visitName(self, node): cmlenz@81: """Overridden to lookup names in the context data instead of in cmlenz@81: locals/globals. cmlenz@81: cmlenz@81: If a name is not found in the context data, we fall back to Python cmlenz@81: builtins. cmlenz@81: """ cmlenz@81: next = self.newBlock() cmlenz@81: end = self.newBlock() cmlenz@30: cmlenz@81: # default: lookup in context data cmlenz@81: self.loadName('data') cmlenz@81: self.emit('LOAD_ATTR', 'get') cmlenz@81: self.emit('LOAD_CONST', node.name) cmlenz@81: self.emit('CALL_FUNCTION', 1) cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: cmlenz@81: # test whether the value "is None" cmlenz@81: self.emit('LOAD_NAME', 'val') cmlenz@81: self.emit('LOAD_CONST', None) cmlenz@81: self.emit('COMPARE_OP', 'is') cmlenz@81: self.emit('JUMP_IF_FALSE', next) cmlenz@81: self.emit('POP_TOP') cmlenz@81: cmlenz@81: # if it is, fallback to builtins cmlenz@81: self.emit('LOAD_GLOBAL', 'getattr') cmlenz@81: self.emit('LOAD_GLOBAL', '__builtin__') cmlenz@81: self.emit('LOAD_CONST', node.name) cmlenz@81: self.emit('LOAD_CONST', None) cmlenz@81: self.emit('CALL_FUNCTION', 3) cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: self.emit('JUMP_FORWARD', end) cmlenz@81: cmlenz@81: self.nextBlock(next) cmlenz@81: self.emit('POP_TOP') cmlenz@81: cmlenz@81: self.nextBlock(end) cmlenz@81: self.emit('LOAD_NAME', 'val') cmlenz@81: cmlenz@81: def visitSubscript(self, node, aug_flag=None): cmlenz@81: """Overridden to fallback to attribute access if the object doesn't cmlenz@81: have an item (or doesn't even support item access). cmlenz@81: cmlenz@81: If either method fails, this returns `None` instead of raising an cmlenz@81: `IndexError`, `KeyError`, or `TypeError`. cmlenz@81: """ cmlenz@81: self.visit(node.expr) cmlenz@81: self.emit('STORE_NAME', 'obj') cmlenz@81: cmlenz@81: if len(node.subs) > 1: cmlenz@81: # For non-scalar subscripts, use the default method cmlenz@81: # FIXME: this should catch exceptions cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: for sub in node.subs: cmlenz@81: self.visit(sub) cmlenz@81: self.emit('BUILD_TUPLE', len(node.subs)) cmlenz@81: self.emit('BINARY_SUBSCR') cmlenz@81: cmlenz@81: else: cmlenz@81: # For a scalar subscript, fallback to attribute access cmlenz@81: # FIXME: Would be nice if we could limit this to string subscripts cmlenz@81: try_ = self.newBlock() cmlenz@81: except_ = self.newBlock() cmlenz@81: return_ = self.newBlock() cmlenz@81: self.emit('SETUP_EXCEPT', except_) cmlenz@81: self.nextBlock(try_) cmlenz@81: self.setups.push((pycodegen.EXCEPT, try_)) cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: self.visit(node.subs[0]) cmlenz@81: self.emit('BINARY_SUBSCR') cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: self.emit('POP_BLOCK') cmlenz@81: self.setups.pop() cmlenz@81: self.emit('JUMP_FORWARD', return_) cmlenz@81: cmlenz@81: self.startBlock(except_) cmlenz@81: self.emit('DUP_TOP') cmlenz@81: self.emit('LOAD_GLOBAL', 'KeyError') cmlenz@81: self.emit('LOAD_GLOBAL', 'IndexError') cmlenz@81: self.emit('LOAD_GLOBAL', 'TypeError') cmlenz@81: self.emit('BUILD_TUPLE', 3) cmlenz@81: self.emit('COMPARE_OP', 'exception match') cmlenz@81: next = self.newBlock() cmlenz@81: self.emit('JUMP_IF_FALSE', next) cmlenz@81: self.nextBlock() cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('LOAD_GLOBAL', 'getattr') # exception handler body cmlenz@81: self.emit('LOAD_NAME', 'obj') cmlenz@81: self.visit(node.subs[0]) cmlenz@81: self.emit('LOAD_CONST', None) cmlenz@81: self.emit('CALL_FUNCTION', 3) cmlenz@81: self.emit('STORE_NAME', 'val') cmlenz@81: self.emit('JUMP_FORWARD', return_) cmlenz@81: self.nextBlock(next) cmlenz@81: self.emit('POP_TOP') cmlenz@81: self.emit('END_FINALLY') cmlenz@81: cmlenz@81: # return cmlenz@81: self.nextBlock(return_) cmlenz@81: self.emit('LOAD_NAME', 'val')