# HG changeset patch # User cmlenz # Date 1154696872 0 # Node ID 203f459e7e260531fbbda5923b51337204d087b0 # Parent aa69c1f34a264475284cf18da98a7af9fcf89dc8 * Support for line numbers in exceptions in expression evaluation (#22). * Fix bug in expression evaluation when item access was used inside a lambda or list comprehension. Thanks to Kevin Dangoor for reporting the problem. diff --git a/markup/eval.py b/markup/eval.py --- a/markup/eval.py +++ b/markup/eval.py @@ -16,6 +16,7 @@ import __builtin__ from compiler import ast, parse from compiler.pycodegen import ExpressionCodeGenerator +import new from markup.core import Stream @@ -99,15 +100,24 @@ tree = xform.visit(tree) if isinstance(filename, unicode): - # pycodegen doesn't like unicode in the filename + # unicode file names not allowed for code objects filename = filename.encode('utf-8', 'replace') - tree.filename = filename or '' + elif not filename: + filename = '' + tree.filename = '' + if lineno <= 0: + lineno = 1 gen = ExpressionCodeGenerator(tree) - if lineno >= 0: - gen.emit('SET_LINENO', lineno) + gen.optimized = True + code = gen.getCode() - return gen.getCode() + # 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 new.code(0, code.co_nlocals, code.co_stacksize, + code.co_flags | 0x0040, code.co_code, code.co_consts, + code.co_names, code.co_varnames, filename, code.co_name, + lineno, code.co_lnotab, (), ()) def _lookup_name(data, name, locals=None): val = data.get(name) @@ -257,37 +267,30 @@ for template expressions. """ - def visitGetattr(self, node, *args, **kwargs): - return ast.CallFunc(ast.Name('_lookup_attr'), - [ast.Name('data'), self.visit(node.expr, *args, **kwargs), - ast.Const(node.attrname)] - ) + def visitGetattr(self, node, locals_=False): + return ast.CallFunc(ast.Name('_lookup_attr'), [ + ast.Name('data'), self.visit(node.expr, locals_=locals_), + ast.Const(node.attrname) + ]) - def visitLambda(self, node, *args, **kwargs): - old_lookup_locals = kwargs.get('lookup_locals', False) - kwargs['lookup_locals'] = True - node.code = self.visit(node.code, *args, **kwargs) + def visitLambda(self, node, locals_=False): + node.code = self.visit(node.code, locals_=True) node.filename = '' # workaround for bug in pycodegen - kwargs['lookup_locals'] = old_lookup_locals return node - def visitListComp(self, node, *args, **kwargs): - old_lookup_locals = kwargs.get('lookup_locals', False) - kwargs['lookup_locals'] = True - node.expr = self.visit(node.expr, *args, **kwargs) - node.quals = map(lambda x: self.visit(x, *args, **kwargs), node.quals) - kwargs['lookup_locals'] = old_lookup_locals + def visitListComp(self, node, locals_=False): + node.expr = self.visit(node.expr, locals_=True) + node.quals = map(lambda x: self.visit(x, locals_=True), node.quals) return node - def visitName(self, node, *args, **kwargs): + def visitName(self, node, locals_=False): func_args = [ast.Name('data'), ast.Const(node.name)] - if kwargs.get('lookup_locals'): + if locals_: func_args.append(ast.CallFunc(ast.Name('locals'), [])) return ast.CallFunc(ast.Name('_lookup_name'), func_args) - return node - def visitSubscript(self, node, *args, **kwargs): - return ast.CallFunc(ast.Name('_lookup_item'), - [ast.Name('data'), self.visit(node.expr, *args, **kwargs), - ast.Tuple(map(self.visit, node.subs, *args, **kwargs))] - ) + def visitSubscript(self, node, locals_=False): + return ast.CallFunc(ast.Name('_lookup_item'), [ + ast.Name('data'), self.visit(node.expr, locals_=locals_), + ast.Tuple(map(lambda x: self.visit(x, locals_=locals_), node.subs)) + ]) diff --git a/markup/tests/eval.py b/markup/tests/eval.py --- a/markup/tests/eval.py +++ b/markup/tests/eval.py @@ -12,6 +12,7 @@ # history and logs, available at hhttp://markup.edgewall.org/log/. import doctest +import sys import unittest from markup.eval import Expression @@ -214,6 +215,29 @@ self.assertEqual([2, 3, 4, 5, 6], expr.evaluate({'numbers': range(5), 'offset': 2})) + def test_list_comprehension_with_getattr(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("[i.name for i in items if i.value > 1]") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_list_comprehension_with_getitem(self): + items = [{'name': 'a', 'value': 1}, {'name': 'b', 'value': 2}] + expr = Expression("[i['name'] for i in items if i['value'] > 1]") + self.assertEqual(['b'], expr.evaluate({'items': items})) + + def test_error_position(self): + expr = Expression("nothing()", filename='index.html', lineno=50) + try: + expr.evaluate({}) + self.fail('Expected TypeError') + except TypeError, e: + exc_type, exc_value, exc_traceback = sys.exc_info() + frame = exc_traceback.tb_next + while frame.tb_next: + frame = frame.tb_next + self.assertEqual('index.html', frame.tb_frame.f_code.co_filename) + self.assertEqual(50, frame.tb_lineno) + def suite(): suite = unittest.TestSuite()