diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 1ea12b32d2..294761672c 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -17,13 +17,34 @@ class classonlymethod(classmethod): return super(classonlymethod, self).__get__(instance, owner) -def method_decorator(decorator): +def method_decorator(decorator, name=''): """ 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 on. - def _dec(func): + # 'obj' can be a class or a function. If 'obj' is a function at the time it + # is passed to _dec, it will eventually be a method of the class it is + # defined on. If 'obj' is a class, the 'name' is required to be the name + # of the method that will be decorated. + def _dec(obj): + is_class = isinstance(obj, type) + if is_class: + if name and hasattr(obj, name): + func = getattr(obj, name) + if not callable(func): + raise TypeError( + "Cannot decorate '{0}' as it isn't a callable " + "attribute of {1} ({2})".format(name, obj, func) + ) + else: + raise ValueError( + "The keyword argument `name` must be the name of a method " + "of the decorated class: {0}. Got '{1}' instead".format( + obj, name, + ) + ) + else: + func = obj + def _wrapper(self, *args, **kwargs): @decorator def bound_func(*args2, **kwargs2): @@ -43,6 +64,10 @@ def method_decorator(decorator): # Need to preserve any existing attributes of 'func', including the name. update_wrapper(_wrapper, func) + if is_class: + setattr(obj, name, _wrapper) + return obj + return _wrapper update_wrapper(_dec, decorator, assigned=available_attrs(decorator)) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index c9deba887a..12bb351df0 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -151,11 +151,17 @@ The functions defined in this module share the following properties: .. module:: django.utils.decorators :synopsis: Functions that help with creating decorators for views. -.. function:: method_decorator(decorator) +.. function:: method_decorator(decorator, name='') - Converts a function decorator into a method decorator. See :ref:`decorating + Converts a function decorator into a method decorator. It can be used to + decorate methods or classes; in the latter case, ``name`` is the name + of the method to be decorated and is required. See :ref:`decorating class based views` for example usage. + .. versionchanged:: 1.9 + + The ability to decorate classes and the ``name`` parameter were added. + .. function:: decorator_from_middleware(middleware_class) Given a middleware class, returns a view decorator. This lets you use diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 82ff5a08bc..f016a08c32 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -338,6 +338,9 @@ Generic Views * Class based views generated using ``as_view()`` now have ``view_class`` and ``view_initkwargs`` attributes. +* :func:`~django.utils.decorators.method_decorator` can now be used to + :ref:`decorate classes instead of methods `. + Internationalization ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt index 6724bec0da..2e5cc1bca1 100644 --- a/docs/topics/class-based-views/intro.txt +++ b/docs/topics/class-based-views/intro.txt @@ -279,8 +279,18 @@ that it can be used on an instance method. For example:: def dispatch(self, *args, **kwargs): return super(ProtectedView, self).dispatch(*args, **kwargs) -In this example, every instance of ``ProtectedView`` will have -login protection. +Or, more succinctly, you can decorate the class instead and pass the name +of the method to be decorated as the keyword argument ``name``:: + + @method_decorator(login_required, name='dispatch') + class ProtectedView(TemplateView): + template_name = 'secret.html' + +.. versionchanged:: 1.9 + + The ability to use ``method_decorator()`` on a class was added. + +In this example, every instance of ``ProtectedView`` will have login protection. .. note:: diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index d6b2c78068..0f0f0f52c2 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -7,6 +7,7 @@ from django.contrib.auth.decorators import ( ) from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed from django.middleware.clickjacking import XFrameOptionsMiddleware +from django.test import SimpleTestCase from django.utils.decorators import method_decorator from django.utils.functional import allow_lazy, lazy from django.views.decorators.cache import ( @@ -189,7 +190,7 @@ class ClsDec(object): return update_wrapper(wrapped, f) -class MethodDecoratorTests(TestCase): +class MethodDecoratorTests(SimpleTestCase): """ Tests for method_decorator """ @@ -274,6 +275,54 @@ class MethodDecoratorTests(TestCase): self.assertEqual(Test().method(1), 1) + def test_class_decoration(self): + """ + @method_decorator can be used to decorate a class and its methods. + """ + def deco(func): + def _wrapper(*args, **kwargs): + return True + return _wrapper + + @method_decorator(deco, name="method") + class Test(object): + def method(self): + return False + + self.assertTrue(Test().method()) + + def test_invalid_non_callable_attribute_decoration(self): + """ + @method_decorator on a non-callable attribute raises an error. + """ + msg = ( + "Cannot decorate 'prop' as it isn't a callable attribute of " + " (1)" + ) + with self.assertRaisesMessage(TypeError, msg): + @method_decorator(lambda: None, name="prop") + class Test(object): + prop = 1 + + @classmethod + def __module__(cls): + return "tests" + + def test_invalid_method_name_to_decorate(self): + """ + @method_decorator on a nonexistent method raises an error. + """ + msg = ( + "The keyword argument `name` must be the name of a method of the " + "decorated class: . Got 'non_existing_method' instead" + ) + with self.assertRaisesMessage(ValueError, msg): + @method_decorator(lambda: None, name="non_existing_method") + class Test(object): + @classmethod + def __module__(cls): + return "tests" + class XFrameOptionsDecoratorsTests(TestCase): """