# HG changeset patch # User cmlenz # Date 1178883148 0 # Node ID 4ed941aa0cbf34b812ea3bf30183e4dfd8e4b7f4 # Parent 76a0ec32835d9c7e8ae5564ce3d41abf002efe5b Apply patch from #113, also closing #114. diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -19,6 +19,12 @@ argument. * The `HTMLFormFiller` stream filter no longer alters form elements for which the data element contains no corresponding item. + * Code in `` processing instructions no longer gets the special + treatment as Python code in template expressions, i.e. item and attribute + access are no longer interchangeable (which was broken in a number of ways + anyway, see ticket #113). This change does not affect expressions. + * Numerous fixes for the execution of Python code in `` processing + instructions (tickets #113 and #114). Version 0.4 diff --git a/genshi/template/eval.py b/genshi/template/eval.py --- a/genshi/template/eval.py +++ b/genshi/template/eval.py @@ -140,7 +140,7 @@ """Executes Python statements used in templates. >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'}) - >>> Suite('foo = dict.some').execute(data) + >>> Suite("foo = dict['some']").execute(data) >>> data['foo'] 'thing' """ @@ -361,7 +361,8 @@ return parse(source, mode) def _compile(node, source=None, mode='eval', filename=None, lineno=-1): - tree = TemplateASTTransformer().visit(node) + xform = {'eval': ExpressionASTTransformer}.get(mode, TemplateASTTransformer) + tree = xform().visit(node) if isinstance(filename, unicode): # unicode file names not allowed for code objects filename = filename.encode('utf-8', 'replace') @@ -397,17 +398,15 @@ Every visitor method can be overridden to return an AST node that has been altered or replaced in some way. """ - _visitors = {} def visit(self, node): if node is None: return None - v = self._visitors.get(node.__class__) - if not v: - v = getattr(self.__class__, 'visit%s' % node.__class__.__name__, - self.__class__._visitDefault) - self._visitors[node.__class__] = v - return v(self, node) + if type(node) is tuple: + return tuple([self.visit(n) for n in node]) + visitor = getattr(self, 'visit%s' % node.__class__.__name__, + self._visitDefault) + return visitor(node) def _visitDefault(self, node): return node @@ -475,10 +474,25 @@ node.expr = self.visit(node.expr) return node + def visitAssAttr(self, node): + node.expr = self.visit(node.expr) + return node + + def visitAugAssign(self, node): + node.node = self.visit(node.node) + node.expr = self.visit(node.expr) + return node + def visitDecorators(self, node): node.nodes = [self.visit(x) for x in node.nodes] return node + def visitExec(self, node): + node.expr = self.visit(node.expr) + node.locals = self.visit(node.locals) + node.globals = self.visit(node.globals) + return node + def visitFor(self, node): node.assign = self.visit(node.assign) node.list = self.visit(node.list) @@ -503,6 +517,10 @@ node.expr3 = self.visit(node.expr3) return node + def visitReturn(self, node): + node.value = self.visit(node.value) + return node + def visitTryExcept(self, node): node.body = self.visit(node.body) node.handlers = self.visit(node.handlers) @@ -536,7 +554,7 @@ node.nodes = [self.visit(x) for x in node.nodes] return node visitAnd = visitOr = visitBitand = visitBitor = visitBitxor = _visitBoolOp - visitAssTuple = _visitBoolOp + visitAssTuple = visitAssList = _visitBoolOp def _visitBinOp(self, node): node.left = self.visit(node.left) @@ -651,6 +669,21 @@ self.locals[-1].add(node.name) return node + def visitAugAssign(self, node): + if isinstance(node.node, ast.Name): + name = node.node.name + node.node = ast.Subscript(ast.Name('data'), 'OP_APPLY', + [ast.Const(name)]) + node.expr = self.visit(node.expr) + return ast.If([ + (ast.Compare(ast.Const(name), [('in', ast.Name('data'))]), + ast.Stmt([node]))], + ast.Stmt([ast.Raise(ast.CallFunc(ast.Name('UndefinedError'), + [ast.Const(name)]), + None, None)])) + else: + return ASTTransformer.visitAugAssign(self, node) + def visitClass(self, node): self.locals.append(set()) node = ASTTransformer.visitClass(self, node) @@ -675,12 +708,6 @@ self.locals.pop() return node - def visitGetattr(self, node): - return ast.CallFunc(ast.Name('_lookup_attr'), [ - ast.Name('data'), self.visit(node.expr), - ast.Const(node.attrname) - ]) - def visitLambda(self, node): self.locals.append(set(flatten(node.argnames))) node = ASTTransformer.visitLambda(self, node) @@ -703,6 +730,18 @@ func_args = [ast.Name('data'), ast.Const(node.name)] return ast.CallFunc(ast.Name('_lookup_name'), func_args) + +class ExpressionASTTransformer(TemplateASTTransformer): + """Concrete AST transformer that implements the AST transformations needed + for code embedded in templates. + """ + + def visitGetattr(self, node): + return ast.CallFunc(ast.Name('_lookup_attr'), [ + ast.Name('data'), self.visit(node.expr), + ast.Const(node.attrname) + ]) + def visitSubscript(self, node): return ast.CallFunc(ast.Name('_lookup_item'), [ ast.Name('data'), self.visit(node.expr), diff --git a/genshi/template/tests/eval.py b/genshi/template/tests/eval.py --- a/genshi/template/tests/eval.py +++ b/genshi/template/tests/eval.py @@ -379,18 +379,19 @@ expr.evaluate({'something': Something()}) self.fail('Expected UndefinedError') except UndefinedError, e: + self.assertEqual(' has no member named "nil"', str(e)) exc_type, exc_value, exc_traceback = sys.exc_info() + search_string = "" frame = exc_traceback.tb_next - frames = [] while frame.tb_next: frame = frame.tb_next - frames.append(frame) - self.assertEqual(' has no member named "nil"', str(e)) - self.assertEqual("", - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) + code = frame.tb_frame.f_code + if code.co_name == search_string: + break + else: + self.fail("never found the frame I was looking for") + self.assertEqual('index.html', code.co_filename) + self.assertEqual(50, frame.tb_lineno) def test_error_getitem_undefined_string(self): class Something(object): @@ -402,18 +403,19 @@ expr.evaluate({'something': Something()}) self.fail('Expected UndefinedError') except UndefinedError, e: + self.assertEqual(' has no member named "nil"', str(e)) exc_type, exc_value, exc_traceback = sys.exc_info() + search_string = '''''' frame = exc_traceback.tb_next - frames = [] while frame.tb_next: frame = frame.tb_next - frames.append(frame) - self.assertEqual(' has no member named "nil"', str(e)) - self.assertEqual('''''', - frames[-3].tb_frame.f_code.co_name) - self.assertEqual('index.html', - frames[-3].tb_frame.f_code.co_filename) - self.assertEqual(50, frames[-3].tb_lineno) + code = frame.tb_frame.f_code + if code.co_name == search_string: + break + else: + self.fail("never found the frame I was looking for") + self.assertEqual('index.html', code.co_filename) + self.assertEqual(50, frame.tb_lineno) class SuiteTestCase(unittest.TestCase): @@ -431,6 +433,16 @@ assert 'donothing' in data self.assertEqual(None, data['donothing']()) + def test_def_with_multiple_statements(self): + suite = Suite("""def donothing(): + if True: + return foo +""") + data = {'foo': 'bar'} + suite.execute(data) + assert 'donothing' in data + self.assertEqual('bar', data['donothing']()) + def test_delete(self): suite = Suite("""foo = 42 del foo @@ -504,6 +516,61 @@ suite.execute(data) self.assertEqual(4, data['x']) + def test_augmented_attribute_assignment(self): + suite = Suite("d['k'] += 42") + d = {"k": 1} + suite.execute({"d": d}) + self.assertEqual(43, d["k"]) + + def test_local_augmented_assign(self): + Suite("x = 1; x += 42; assert x == 43").execute({}) + + def test_assign_in_list(self): + suite = Suite("[d['k']] = 'foo',; assert d['k'] == 'foo'") + d = {"k": "bar"} + suite.execute({"d": d}) + self.assertEqual("foo", d["k"]) + + def test_exec(self): + suite = Suite("x = 1; exec d['k']; assert x == 42, x") + suite.execute({"d": {"k": "x = 42"}}) + + def test_return(self): + suite = Suite(""" +def f(): + return v + +assert f() == 42 +""") + suite.execute({"v": 42}) + + def test_assign_to_dict_item(self): + suite = Suite("d['k'] = 'foo'") + data = {'d': {}} + suite.execute(data) + self.assertEqual('foo', data['d']['k']) + + def test_assign_to_attribute(self): + class Something(object): pass + something = Something() + suite = Suite("obj.attr = 'foo'") + data = {"obj": something} + suite.execute(data) + self.assertEqual('foo', something.attr) + + def test_delattr(self): + class Something(object): + def __init__(self): + self.attr = 'foo' + obj = Something() + Suite("del obj.attr").execute({'obj': obj}) + self.failIf(hasattr(obj, 'attr')) + + def test_delitem(self): + d = {'k': 'foo'} + Suite("del d['k']").execute({'d': d}) + self.failIf('k' in d, `d`) + def suite(): suite = unittest.TestSuite()