Pārlūkot izejas kodu

Fixed #25146 -- Allowed method_decorator() to decorate classes.

Rigel Di Scala 9 gadi atpakaļ
vecāks
revīzija
3bdaaf6777

+ 29 - 4
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))

+ 8 - 2
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<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

+ 3 - 0
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 <decorating-class-based-views>`.
+
 Internationalization
 ^^^^^^^^^^^^^^^^^^^^
 

+ 12 - 2
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::
 

+ 50 - 1
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 "
+            "<class 'Test'> (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: <class 'Test'>. 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):
     """