diff --git a/django/utils/decorators.py b/django/utils/decorators.py index c3bf7bd49f..9f8b041e4b 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -2,7 +2,7 @@ # For backwards compatibility in Django 2.0. from contextlib import ContextDecorator # noqa -from functools import WRAPPER_ASSIGNMENTS, update_wrapper, wraps +from functools import WRAPPER_ASSIGNMENTS, partial, update_wrapper, wraps class classonlymethod(classmethod): @@ -36,8 +36,10 @@ def _multi_decorate(decorators, method): def _wrapper(self, *args, **kwargs): # bound_method has the signature that 'decorator' expects i.e. no - # 'self' argument. - bound_method = method.__get__(self, type(self)) + # 'self' argument, but it's a closure over self so it can call + # 'func'. Also, wrap method.__get__() in a function because new + # attributes can't be set on bound method objects, only on functions. + bound_method = partial(method.__get__(self, type(self))) for dec in decorators: bound_method = dec(bound_method) return bound_method(*args, **kwargs) diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index e8f6b8d2d5..aaa09c0056 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -271,6 +271,21 @@ class MethodDecoratorTests(SimpleTestCase): self.assertEqual(Test.method.__doc__, 'A method') self.assertEqual(Test.method.__name__, 'method') + def test_new_attribute(self): + """A decorator that sets a new attribute on the method.""" + def decorate(func): + func.x = 1 + return func + + class MyClass: + @method_decorator(decorate) + def method(self): + return True + + obj = MyClass() + self.assertEqual(obj.method.x, 1) + self.assertIs(obj.method(), True) + def test_bad_iterable(self): decorators = {myattr_dec_m, myattr2_dec_m} msg = "'set' object is not subscriptable"