diff --git a/django/utils/decorators.py b/django/utils/decorators.py index ba276937f81..d75d36f753b 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -11,15 +11,28 @@ def method_decorator(decorator): """ Converts a function decorator into a method decorator """ + # 'func' is a function at the time it is passed to _dec, but will eventually + # be a method of the class it is defined it. def _dec(func): def _wrapper(self, *args, **kwargs): + @decorator def bound_func(*args2, **kwargs2): return func(self, *args2, **kwargs2) # bound_func has the signature that 'decorator' expects i.e. no # 'self' argument, but it is a closure over self so it can call # 'func' correctly. - return decorator(bound_func)(*args, **kwargs) - return wraps(func)(_wrapper) + return bound_func(*args, **kwargs) + # In case 'decorator' adds attributes to the function it decorates, we + # want to copy those. We don't have access to bound_func in this scope, + # but we can cheat by using it on a dummy function. + @decorator + def dummy(*args, **kwargs): + pass + update_wrapper(_wrapper, dummy) + # Need to preserve any existing attributes of 'func', including the name. + update_wrapper(_wrapper, func) + + return _wrapper update_wrapper(_dec, decorator) # Change the name to aid debugging. _dec.__name__ = 'method_decorator(%s)' % decorator.__name__ diff --git a/tests/regressiontests/decorators/tests.py b/tests/regressiontests/decorators/tests.py index b2c0c0128b4..ba0aa3535f5 100644 --- a/tests/regressiontests/decorators/tests.py +++ b/tests/regressiontests/decorators/tests.py @@ -126,14 +126,60 @@ def simple_dec(func): simple_dec_m = method_decorator(simple_dec) +# For testing method_decorator, two decorators that add an attribute to the function +def myattr_dec(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + wrapper.myattr = True + return wraps(func)(wrapper) + +myattr_dec_m = method_decorator(myattr_dec) + + +def myattr2_dec(func): + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + wrapper.myattr2 = True + return wraps(func)(wrapper) + +myattr2_dec_m = method_decorator(myattr2_dec) + + class MethodDecoratorTests(TestCase): """ Tests for method_decorator """ - def test_method_decorator(self): + def test_preserve_signature(self): class Test(object): @simple_dec_m def say(self, arg): return arg self.assertEqual("test:hello", Test().say("hello")) + + def test_preserve_attributes(self): + # Sanity check myattr_dec and myattr2_dec + @myattr_dec + @myattr2_dec + def func(): + pass + + self.assertEqual(getattr(func, 'myattr', False), True) + self.assertEqual(getattr(func, 'myattr2', False), True) + + # Now check method_decorator + class Test(object): + @myattr_dec_m + @myattr2_dec_m + def method(self): + "A method" + pass + + self.assertEqual(getattr(Test().method, 'myattr', False), True) + self.assertEqual(getattr(Test().method, 'myattr2', False), True) + + self.assertEqual(getattr(Test.method, 'myattr', False), True) + self.assertEqual(getattr(Test.method, 'myattr2', False), True) + + self.assertEqual(Test.method.__doc__, 'A method') + self.assertEqual(Test.method.im_func.__name__, 'method')