diff markup/eval.py @ 81:cc034182061e

Template expressions are now compiled to Python bytecode.
author cmlenz
date Sat, 15 Jul 2006 11:29:25 +0000
parents e9a3930f8823
children c82c002d4c32
line wrap: on
line diff
--- a/markup/eval.py
+++ b/markup/eval.py
@@ -16,12 +16,7 @@
 from __future__ import division
 
 import __builtin__
-try:
-    import _ast # Python 2.5
-except ImportError:
-    _ast = None
-    import compiler
-import operator
+from compiler import parse, pycodegen
 
 from markup.core import Stream
 
@@ -34,6 +29,7 @@
     >>> 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)
@@ -46,7 +42,8 @@
     
     >>> Expression('dict.some').evaluate(data)
     'thing'
-    
+    """
+    """
     This also works the other way around: item access can be used to access
     any object attribute (meaning there's no use for `getattr()` in templates):
     
@@ -68,292 +65,210 @@
     >>> Expression('len(items)').evaluate(data)
     3
     """
-    __slots__ = ['source', 'ast']
+    __slots__ = ['source', 'code']
     _visitors = {}
 
-    def __init__(self, source):
+    def __init__(self, source, filename=None, lineno=-1):
         """Create the expression.
         
         @param source: the expression as string
         """
         self.source = source
-        self.ast = None
+
+        tree = parse(self.source, 'eval')
+        if isinstance(filename, unicode):
+            # pycodegen doesn't like unicode in the filename
+            filename = filename.encode('utf-8', 'replace')
+        tree.filename = filename or '<string>'
+        gen = TemplateExpressionCodeGenerator(tree)
+        if lineno >= 0:
+            gen.emit('SET_LINENO', lineno)
+        self.code = gen.getCode()
 
     def __repr__(self):
         return '<Expression "%s">' % self.source
 
-    if _ast is None:
-
-        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
-            """
-            if not self.ast:
-                self.ast = compiler.parse(self.source, 'eval')
-            return self._visit(self.ast.node, data)
-
-        # AST traversal
-
-        def _visit(self, node, data):
-            v = self._visitors.get(node.__class__)
-            if not v:
-                v = getattr(self, '_visit_%s' % node.__class__.__name__.lower())
-                self._visitors[node.__class__] = v
-            return v(node, data)
-
-        def _visit_expression(self, node, data):
-            for child in node.getChildNodes():
-                return self._visit(child, data)
-
-        # Functions & Accessors
-
-        def _visit_callfunc(self, node, data):
-            func = self._visit(node.node, data)
-            if func is None:
-                return None
-            args = [self._visit(arg, data) for arg in node.args
-                    if not isinstance(arg, compiler.ast.Keyword)]
-            kwargs = dict([(arg.name, self._visit(arg.expr, data)) for arg
-                           in node.args if isinstance(arg, compiler.ast.Keyword)])
-            return func(*args, **kwargs)
-
-        def _visit_getattr(self, node, data):
-            obj = self._visit(node.expr, data)
-            if hasattr(obj, node.attrname):
-                return getattr(obj, node.attrname)
-            try:
-                return obj[node.attrname]
-            except (KeyError, TypeError):
-                return None
-
-        def _visit_slice(self, node, data):
-            obj = self._visit(node.expr, data)
-            lower = node.lower and self._visit(node.lower, data) or None
-            upper = node.upper and self._visit(node.upper, data) or None
-            return obj[lower:upper]
-
-        def _visit_subscript(self, node, data):
-            obj = self._visit(node.expr, data)
-            subs = map(lambda sub: self._visit(sub, data), node.subs)
-            if len(subs) == 1:
-                subs = subs[0]
-            try:
-                return obj[subs]
-            except (KeyError, IndexError, TypeError):
-                try:
-                    return getattr(obj, subs)
-                except (AttributeError, TypeError):
-                    return None
-
-        # Operators
-
-        def _visit_and(self, node, data):
-            return reduce(lambda x, y: x and y,
-                          [self._visit(n, data) for n in node.nodes])
-
-        def _visit_or(self, node, data):
-            return reduce(lambda x, y: x or y,
-                          [self._visit(n, data) for n in node.nodes])
-
-        def _visit_bitand(self, node, data):
-            return reduce(operator.and_,
-                          [self._visit(n, data) for n in node.nodes])
-
-        def _visit_bitor(self, node, data):
-            return reduce(operator.or_,
-                          [self._visit(n, data) for n in node.nodes])
-
-        _OP_MAP = {'==': operator.eq, '!=': operator.ne,
-                   '<':  operator.lt, '<=': operator.le,
-                   '>':  operator.gt, '>=': operator.ge,
-                   'is': operator.is_, 'is not': operator.is_not,
-                   'in': lambda x, y: operator.contains(y, x),
-                   'not in': lambda x, y: not operator.contains(y, x)}
-        def _visit_compare(self, node, data):
-            result = self._visit(node.expr, data)
-            ops = node.ops[:]
-            ops.reverse()
-            for op, rval in ops:
-                result = self._OP_MAP[op](result, self._visit(rval, data))
-            return result
-
-        def _visit_add(self, node, data):
-            return self._visit(node.left, data) + self._visit(node.right, data)
-
-        def _visit_div(self, node, data):
-            return self._visit(node.left, data) / self._visit(node.right, data)
-
-        def _visit_floordiv(self, node, data):
-            return self._visit(node.left, data) // self._visit(node.right, data)
-
-        def _visit_mod(self, node, data):
-            return self._visit(node.left, data) % self._visit(node.right, data)
-
-        def _visit_mul(self, node, data):
-            return self._visit(node.left, data) * self._visit(node.right, data)
-
-        def _visit_power(self, node, data):
-            return self._visit(node.left, data) ** self._visit(node.right, data)
-
-        def _visit_sub(self, node, data):
-            return self._visit(node.left, data) - self._visit(node.right, data)
-
-        def _visit_unaryadd(self, node, data):
-            return +self._visit(node.expr, data)
-
-        def _visit_unarysub(self, node, data):
-            return -self._visit(node.expr, data)
-
-        def _visit_not(self, node, data):
-            return not self._visit(node.expr, data)
-
-        def _visit_invert(self, node, data):
-            return ~self._visit(node.expr, data)
-
-        # Identifiers & Literals
+    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
+        """
+        return eval(self.code)
 
-        def _visit_name(self, node, data):
-            val = data.get(node.name)
-            if val is None:
-                val = getattr(__builtin__, node.name, None)
-            return val
-
-        def _visit_const(self, node, data):
-            return node.value
-
-        def _visit_dict(self, node, data):
-            return dict([(self._visit(k, data), self._visit(v, data))
-                         for k, v in node.items])
-
-        def _visit_tuple(self, node, data):
-            return tuple([self._visit(n, data) for n in node.nodes])
-
-        def _visit_list(self, node, data):
-            return [self._visit(n, data) for n in node.nodes]
-
-    else:
-
-        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
-            """
-            if not self.ast:
-                self.ast = compile(self.source, '?', 'eval', 0x400)
-            return self._visit(self.ast, data)
-
-        # AST traversal
-
-        def _visit(self, node, data):
-            v = self._visitors.get(node.__class__)
-            if not v:
-                v = getattr(self, '_visit_%s' % node.__class__.__name__.lower())
-                self._visitors[node.__class__] = v
-            return v(node, data)
-
-        def _visit_expression(self, node, data):
-            return self._visit(node.body, data)
-
-        # Functions & Accessors
-
-        def _visit_attribute(self, node, data):
-            obj = self._visit(node.value, data)
-            if hasattr(obj, node.attr):
-                return getattr(obj, node.attr)
-            try:
-                return obj[node.attr]
-            except (KeyError, TypeError):
-                return None
-
-        def _visit_call(self, node, data):
-            func = self._visit(node.func, data)
-            if func is None:
-                return None
-            args = [self._visit(arg, data) for arg in node.args]
-            kwargs = dict([(kwarg.arg, self._visit(kwarg.value, data))
-                           for kwarg in node.keywords])
-            return func(*args, **kwargs)
 
-        def _visit_subscript(self, node, data):
-            obj = self._visit(node.value, data)
-            if isinstance(node.slice, _ast.Slice):
-                try:
-                    return obj[self._visit(lower, data):
-                               self._visit(upper, data):
-                               self._visit(step, data)]
-                except (KeyError, IndexError, TypeError):
-                    pass
-            else:
-                index = self._visit(node.slice.value, data)
-                try:
-                    return obj[index]
-                except (KeyError, IndexError, TypeError):
-                    try:
-                        return getattr(obj, index)
-                    except (AttributeError, TypeError):
-                        pass
-            return None
-
-        # Operators
-
-        _OP_MAP = {_ast.Add: operator.add, _ast.And: lambda l, r: l and r,
-                   _ast.BitAnd: operator.and_, _ast.BitOr: operator.or_,
-                   _ast.Div: operator.truediv, _ast.Eq: operator.eq,
-                   _ast.FloorDiv: operator.floordiv, _ast.Gt: operator.gt,
-                   _ast.GtE: operator.ge, _ast.Invert: operator.inv,
-                   _ast.In: lambda l, r: operator.contains(r, l),
-                   _ast.Is: operator.is_, _ast.IsNot: operator.is_not,
-                   _ast.Lt: operator.lt, _ast.LtE: operator.le,
-                   _ast.Mod: operator.mod, _ast.Mult: operator.mul,
-                   _ast.Not: operator.not_, _ast.NotEq: operator.ne,
-                   _ast.NotIn: lambda l, r: not operator.contains(r, l),
-                   _ast.Or: lambda l, r: l or r, _ast.Pow: operator.pow,
-                   _ast.Sub: operator.sub, _ast.UAdd: operator.pos,
-                   _ast.USub: operator.neg}
-
-        def _visit_unaryop(self, node, data):
-            return self._OP_MAP[node.op.__class__](self._visit(node.operand, data))
+class TemplateExpressionCodeGenerator(pycodegen.ExpressionCodeGenerator):
 
-        def _visit_binop(self, node, data):
-            return self._OP_MAP[node.op.__class__](self._visit(node.left, data),
-                                                   self._visit(node.right, data))
-
-        def _visit_boolop(self, node, data):
-            return reduce(self._OP_MAP[node.op.__class__],
-                          [self._visit(n, data) for n in node.values])
-
-        def _visit_compare(self, node, data):
-            result = self._visit(node.left, data)
-            ops = node.ops[:]
-            ops.reverse()
-            for op, rval in zip(ops, node.comparators):
-                result = self._OP_MAP[op.__class__](result,
-                                                     self._visit(rval, data))
-            return result
-
-        # Identifiers & Literals
+    def visitGetattr(self, node):
+        """Overridden to fallback to item access if the object doesn't have an
+        attribute.
+        
+        Also, if either method fails, this returns `None` instead of raising an
+        `AttributeError`.
+        """
+        # check whether the object has the request attribute
+        self.visit(node.expr)
+        self.emit('STORE_NAME', 'obj')
+        self.emit('LOAD_GLOBAL', 'hasattr')
+        self.emit('LOAD_NAME', 'obj')
+        self.emit('LOAD_CONST', node.attrname)
+        self.emit('CALL_FUNCTION', 2)
+        else_ = self.newBlock()
+        self.emit('JUMP_IF_FALSE', else_)
+        self.emit('POP_TOP')
 
-        def _visit_dict(self, node, data):
-            return dict([(self._visit(k, data), self._visit(v, data))
-                         for k, v in zip(node.keys, node.values)])
-
-        def _visit_list(self, node, data):
-            return [self._visit(n, data) for n in node.elts]
+        # hasattr returned True, so return the attribute value
+        self.emit('LOAD_NAME', 'obj')
+        self.emit('LOAD_ATTR', node.attrname)
+        self.emit('STORE_NAME', 'val')
+        return_ = self.newBlock()
+        self.emit('JUMP_FORWARD', return_)
 
-        def _visit_name(self, node, data):
-            val = data.get(node.id)
-            if val is None:
-                val = getattr(__builtin__, node.id, None)
-            return val
+        # hasattr returned False, so try item access
+        self.startBlock(else_)
+        try_ = self.newBlock()
+        except_ = self.newBlock()
+        self.emit('SETUP_EXCEPT', except_)
+        self.nextBlock(try_)
+        self.setups.push((pycodegen.EXCEPT, try_))
+        self.emit('LOAD_NAME', 'obj')
+        self.emit('LOAD_CONST', node.attrname)
+        self.emit('BINARY_SUBSCR')
+        self.emit('STORE_NAME', 'val')
+        self.emit('POP_BLOCK')
+        self.setups.pop()
+        self.emit('JUMP_FORWARD', return_)
 
-        def _visit_num(self, node, data):
-            return node.n
+        # exception handler: just return `None`
+        self.startBlock(except_)
+        self.emit('DUP_TOP')
+        self.emit('LOAD_GLOBAL', 'KeyError')
+        self.emit('LOAD_GLOBAL', 'TypeError')
+        self.emit('BUILD_TUPLE', 2)
+        self.emit('COMPARE_OP', 'exception match')
+        next = self.newBlock()
+        self.emit('JUMP_IF_FALSE', next)
+        self.nextBlock()
+        self.emit('POP_TOP')
+        self.emit('POP_TOP')
+        self.emit('POP_TOP')
+        self.emit('POP_TOP')
+        self.emit('LOAD_CONST', None) # exception handler body
+        self.emit('STORE_NAME', 'val')
+        self.emit('JUMP_FORWARD', return_)
+        self.nextBlock(next)
+        self.emit('POP_TOP')
+        self.emit('END_FINALLY')
+        
+        # return
+        self.nextBlock(return_)
+        self.emit('LOAD_NAME', 'val')
 
-        def _visit_str(self, node, data):
-            return node.s
+    def visitName(self, node):
+        """Overridden to lookup names in the context data instead of in
+        locals/globals.
+        
+        If a name is not found in the context data, we fall back to Python
+        builtins.
+        """
+        next = self.newBlock()
+        end = self.newBlock()
 
-        def _visit_tuple(self, node, data):
-            return tuple([self._visit(n, data) for n in node.elts])
+        # default: lookup in context data
+        self.loadName('data')
+        self.emit('LOAD_ATTR', 'get')
+        self.emit('LOAD_CONST', node.name)
+        self.emit('CALL_FUNCTION', 1)
+        self.emit('STORE_NAME', 'val')
+
+        # test whether the value "is None"
+        self.emit('LOAD_NAME', 'val')
+        self.emit('LOAD_CONST', None)
+        self.emit('COMPARE_OP', 'is')
+        self.emit('JUMP_IF_FALSE', next)
+        self.emit('POP_TOP')
+
+        # if it is, fallback to builtins
+        self.emit('LOAD_GLOBAL', 'getattr')
+        self.emit('LOAD_GLOBAL', '__builtin__')
+        self.emit('LOAD_CONST', node.name)
+        self.emit('LOAD_CONST', None)
+        self.emit('CALL_FUNCTION', 3)
+        self.emit('STORE_NAME', 'val')
+        self.emit('JUMP_FORWARD', end)
+
+        self.nextBlock(next)
+        self.emit('POP_TOP')
+
+        self.nextBlock(end)
+        self.emit('LOAD_NAME', 'val')
+
+    def visitSubscript(self, node, aug_flag=None):
+        """Overridden to fallback to attribute access if the object doesn't
+        have an item (or doesn't even support item access).
+        
+        If either method fails, this returns `None` instead of raising an
+        `IndexError`, `KeyError`, or `TypeError`.
+        """
+        self.visit(node.expr)
+        self.emit('STORE_NAME', 'obj')
+
+        if len(node.subs) > 1:
+            # For non-scalar subscripts, use the default method
+            # FIXME: this should catch exceptions
+            self.emit('LOAD_NAME', 'obj')
+            for sub in node.subs:
+                self.visit(sub)
+            self.emit('BUILD_TUPLE', len(node.subs))
+            self.emit('BINARY_SUBSCR')
+
+        else:
+            # For a scalar subscript, fallback to attribute access
+            # FIXME: Would be nice if we could limit this to string subscripts
+            try_ = self.newBlock()
+            except_ = self.newBlock()
+            return_ = self.newBlock()
+            self.emit('SETUP_EXCEPT', except_)
+            self.nextBlock(try_)
+            self.setups.push((pycodegen.EXCEPT, try_))
+            self.emit('LOAD_NAME', 'obj')
+            self.visit(node.subs[0])
+            self.emit('BINARY_SUBSCR')
+            self.emit('STORE_NAME', 'val')
+            self.emit('POP_BLOCK')
+            self.setups.pop()
+            self.emit('JUMP_FORWARD', return_)
+
+            self.startBlock(except_)
+            self.emit('DUP_TOP')
+            self.emit('LOAD_GLOBAL', 'KeyError')
+            self.emit('LOAD_GLOBAL', 'IndexError')
+            self.emit('LOAD_GLOBAL', 'TypeError')
+            self.emit('BUILD_TUPLE', 3)
+            self.emit('COMPARE_OP', 'exception match')
+            next = self.newBlock()
+            self.emit('JUMP_IF_FALSE', next)
+            self.nextBlock()
+            self.emit('POP_TOP')
+            self.emit('POP_TOP')
+            self.emit('POP_TOP')
+            self.emit('POP_TOP')
+            self.emit('LOAD_GLOBAL', 'getattr') # exception handler body
+            self.emit('LOAD_NAME', 'obj')
+            self.visit(node.subs[0])
+            self.emit('LOAD_CONST', None)
+            self.emit('CALL_FUNCTION', 3)
+            self.emit('STORE_NAME', 'val')
+            self.emit('JUMP_FORWARD', return_)
+            self.nextBlock(next)
+            self.emit('POP_TOP')
+            self.emit('END_FINALLY')
+        
+            # return
+            self.nextBlock(return_)
+            self.emit('LOAD_NAME', 'val')
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()
Copyright (C) 2012-2017 Edgewall Software