diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 5c9a5d01c7..69aca10a4d 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -37,7 +37,7 @@ def _multi_decorate(decorators, method): # '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))) + bound_method = wraps(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 46b01c1852..e496e2c790 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -425,6 +425,29 @@ class MethodDecoratorTests(SimpleTestCase): def __module__(cls): return "tests" + def test_wrapper_assignments(self): + """@method_decorator preserves wrapper assignments.""" + func_name = None + func_module = None + + def decorator(func): + @wraps(func) + def inner(*args, **kwargs): + nonlocal func_name, func_module + func_name = getattr(func, '__name__', None) + func_module = getattr(func, '__module__', None) + return func(*args, **kwargs) + return inner + + class Test: + @method_decorator(decorator) + def method(self): + return 'tests' + + Test().method() + self.assertEqual(func_name, 'method') + self.assertIsNotNone(func_module) + class XFrameOptionsDecoratorsTests(TestCase): """