diff options
author | Michael W. Hudson <mwh@python.net> | 2004-08-17 17:29:16 (GMT) |
---|---|---|
committer | Michael W. Hudson <mwh@python.net> | 2004-08-17 17:29:16 (GMT) |
commit | 0ccff074cd806bc7e963a1e0ff6ce25e0d11cce9 (patch) | |
tree | c8e7be65d3051aa878fe53169b9bdeb4d3b4227e /Lib | |
parent | b51b23405b6cf82ab4ec20f3d5ef4b895bd0786f (diff) | |
download | cpython-0ccff074cd806bc7e963a1e0ff6ce25e0d11cce9.zip cpython-0ccff074cd806bc7e963a1e0ff6ce25e0d11cce9.tar.gz cpython-0ccff074cd806bc7e963a1e0ff6ce25e0d11cce9.tar.bz2 |
This is Mark Russell's patch:
[ 1009560 ] Fix @decorator evaluation order
From the description:
Changes in this patch:
- Change Grammar/Grammar to require
newlines between adjacent decorators.
- Fix order of evaluation of decorators
in the C (compile.c) and python
(Lib/compiler/pycodegen.py) compilers
- Add better order of evaluation check
to test_decorators.py (test_eval_order)
- Update the decorator documentation in
the reference manual (improve description
of evaluation order and update syntax
description)
and the comment:
Used Brett's evaluation order (see
http://mail.python.org/pipermail/python-dev/2004-August/047835.html)
(I'm checking this in for Anthony who was having problems getting SF to
talk to him)
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/compiler/pycodegen.py | 4 | ||||
-rw-r--r-- | Lib/compiler/transformer.py | 17 | ||||
-rw-r--r-- | Lib/test/test_decorators.py | 102 |
3 files changed, 88 insertions, 35 deletions
diff --git a/Lib/compiler/pycodegen.py b/Lib/compiler/pycodegen.py index e5d1ff1..02e7764 100644 --- a/Lib/compiler/pycodegen.py +++ b/Lib/compiler/pycodegen.py @@ -367,12 +367,12 @@ class CodeGenerator: def _visitFuncOrLambda(self, node, isLambda=0): if not isLambda and node.decorators: - for decorator in reversed(node.decorators.nodes): + for decorator in node.decorators.nodes: self.visit(decorator) ndecorators = len(node.decorators.nodes) else: ndecorators = 0 - + gen = self.FunctionGen(node, self.scopes, isLambda, self.class_name, self.get_module()) walk(node.code, gen) diff --git a/Lib/compiler/transformer.py b/Lib/compiler/transformer.py index 53a0fda..cdeb5ff 100644 --- a/Lib/compiler/transformer.py +++ b/Lib/compiler/transformer.py @@ -201,13 +201,14 @@ class Transformer: def decorator(self, nodelist): # '@' dotted_name [ '(' [arglist] ')' ] - assert len(nodelist) in (2, 4, 5) + assert len(nodelist) in (3, 5, 6) assert nodelist[0][0] == token.AT + assert nodelist[-1][0] == token.NEWLINE assert nodelist[1][0] == symbol.dotted_name funcname = self.decorator_name(nodelist[1][1:]) - if len(nodelist) > 2: + if len(nodelist) > 3: assert nodelist[2][0] == token.LPAR expr = self.com_call_function(funcname, nodelist[3]) else: @@ -217,16 +218,10 @@ class Transformer: def decorators(self, nodelist): # decorators: decorator ([NEWLINE] decorator)* NEWLINE - listlen = len(nodelist) - i = 0 items = [] - while i < listlen: - assert nodelist[i][0] == symbol.decorator - items.append(self.decorator(nodelist[i][1:])) - i += 1 - - if i < listlen and nodelist[i][0] == token.NEWLINE: - i += 1 + for dec_nodelist in nodelist: + assert dec_nodelist[0] == symbol.decorator + items.append(self.decorator(dec_nodelist[1:])) return Decorators(items) def funcdef(self, nodelist): diff --git a/Lib/test/test_decorators.py b/Lib/test/test_decorators.py index 8010ea3..c4eb8be 100644 --- a/Lib/test/test_decorators.py +++ b/Lib/test/test_decorators.py @@ -40,14 +40,12 @@ def dbcheck(exprstr, globals=None, locals=None): def countcalls(counts): "Decorator to count calls to a function" def decorate(func): - name = func.func_name - counts[name] = 0 + func_name = func.func_name + counts[func_name] = 0 def call(*args, **kwds): - counts[name] += 1 + counts[func_name] += 1 return func(*args, **kwds) - # XXX: Would like to say: call.func_name = func.func_name here - # to make nested decorators work in any order, but func_name - # is a readonly attribute + call.func_name = func_name return call return decorate @@ -65,6 +63,7 @@ def memoize(func): except TypeError: # Unhashable argument return func(*args) + call.func_name = func.func_name return call # ----------------------------------------------- @@ -126,13 +125,13 @@ class TestDecorators(unittest.TestCase): self.assertRaises(DbcheckError, f, 1, None) def test_memoize(self): - # XXX: This doesn't work unless memoize is the last decorator - - # see the comment in countcalls. counts = {} + @memoize @countcalls(counts) def double(x): return x * 2 + self.assertEqual(double.func_name, 'double') self.assertEqual(counts, dict(double=0)) @@ -162,6 +161,11 @@ class TestDecorators(unittest.TestCase): codestr = "@%s\ndef f(): pass" % expr self.assertRaises(SyntaxError, compile, codestr, "test", "exec") + # You can't put multiple decorators on a single line: + # + self.assertRaises(SyntaxError, compile, + "@f1 @f2\ndef f(): pass", "test", "exec") + # Test runtime errors def unimp(func): @@ -187,20 +191,74 @@ class TestDecorators(unittest.TestCase): self.assertEqual(C.foo.booh, 42) def test_order(self): - # Test that decorators are conceptually applied right-recursively; - # that means bottom-up - def ordercheck(num): - def deco(func): - return lambda: num - return deco - - # Should go ordercheck(1)(ordercheck(2)(blah)) which should lead to - # blah() == 1 - @ordercheck(1) - @ordercheck(2) - def blah(): pass - self.assertEqual(blah(), 1, "decorators are meant to be applied " - "bottom-up") + class C(object): + @staticmethod + @funcattrs(abc=1) + def foo(): return 42 + # This wouldn't work if staticmethod was called first + self.assertEqual(C.foo(), 42) + self.assertEqual(C().foo(), 42) + + def test_eval_order(self): + # Evaluating a decorated function involves four steps for each + # decorator-maker (the function that returns a decorator): + # + # 1: Evaluate the decorator-maker name + # 2: Evaluate the decorator-maker arguments (if any) + # 3: Call the decorator-maker to make a decorator + # 4: Call the decorator + # + # When there are multiple decorators, these steps should be + # performed in the above order for each decorator, but we should + # iterate through the decorators in the reverse of the order they + # appear in the source. + + actions = [] + + def make_decorator(tag): + actions.append('makedec' + tag) + def decorate(func): + actions.append('calldec' + tag) + return func + return decorate + + class NameLookupTracer (object): + def __init__(self, index): + self.index = index + + def __getattr__(self, fname): + if fname == 'make_decorator': + opname, res = ('evalname', make_decorator) + elif fname == 'arg': + opname, res = ('evalargs', str(self.index)) + else: + assert False, "Unknown attrname %s" % fname + actions.append('%s%d' % (opname, self.index)) + return res + + c1, c2, c3 = map(NameLookupTracer, [ 1, 2, 3 ]) + + expected_actions = [ 'evalname1', 'evalargs1', 'makedec1', + 'evalname2', 'evalargs2', 'makedec2', + 'evalname3', 'evalargs3', 'makedec3', + 'calldec3', 'calldec2', 'calldec1' ] + + actions = [] + @c1.make_decorator(c1.arg) + @c2.make_decorator(c2.arg) + @c3.make_decorator(c3.arg) + def foo(): return 42 + self.assertEqual(foo(), 42) + + self.assertEqual(actions, expected_actions) + + # Test the equivalence claim in chapter 7 of the reference manual. + # + actions = [] + def bar(): return 42 + bar = c1.make_decorator(c1.arg)(c2.make_decorator(c2.arg)(c3.make_decorator(c3.arg)(bar))) + self.assertEqual(bar(), 42) + self.assertEqual(actions, expected_actions) def test_main(): test_support.run_unittest(TestDecorators) |