فهرست منبع

Fixed #25269 -- Allowed method_decorator() to accept a list/tuple of decorators.

fabrizio ettore messina 9 سال پیش
والد
کامیت
186eb21dc1
5فایلهای تغییر یافته به همراه126 افزوده شده و 18 حذف شده
  1. 18 4
      django/utils/decorators.py
  2. 11 3
      docs/ref/utils.txt
  3. 3 2
      docs/releases/1.9.txt
  4. 21 1
      docs/topics/class-based-views/intro.txt
  5. 73 8
      tests/decorators/tests.py

+ 18 - 4
django/utils/decorators.py

@@ -45,8 +45,20 @@ def method_decorator(decorator, name=''):
         else:
             func = obj
 
+        def decorate(function):
+            """
+            Apply a list/tuple of decorators if decorator is one. Decorator
+            functions are applied so that the call order is the same as the
+            order in which they appear in the iterable.
+            """
+            if hasattr(decorator, '__iter__'):
+                for dec in decorator[::-1]:
+                    function = dec(function)
+                return function
+            return decorator(function)
+
         def _wrapper(self, *args, **kwargs):
-            @decorator
+            @decorate
             def bound_func(*args2, **kwargs2):
                 return func.__get__(self, type(self))(*args2, **kwargs2)
             # bound_func has the signature that 'decorator' expects i.e.  no
@@ -57,7 +69,7 @@ def method_decorator(decorator, name=''):
         # 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
+        @decorate
         def dummy(*args, **kwargs):
             pass
         update_wrapper(_wrapper, dummy)
@@ -69,8 +81,10 @@ def method_decorator(decorator, name=''):
             return obj
 
         return _wrapper
-
-    update_wrapper(_dec, decorator, assigned=available_attrs(decorator))
+    # Don't worry about making _dec look similar to a list/tuple as it's rather
+    # meaningless.
+    if not hasattr(decorator, '__iter__'):
+        update_wrapper(_dec, decorator, assigned=available_attrs(decorator))
     # Change the name to aid debugging.
     if hasattr(decorator, '__name__'):
         _dec.__name__ = 'method_decorator(%s)' % decorator.__name__

+ 11 - 3
docs/ref/utils.txt

@@ -155,12 +155,20 @@ The functions defined in this module share the following properties:
 
     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.
+    of the method to be decorated and is required.
+
+    ``decorator`` may also be a a list or tuple of functions. They are wrapped
+    in reverse order so that the call order is the order in which the functions
+    appear in the list/tuple.
+
+    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.
+       The ability to decorate classes, the ``name`` parameter, and the ability
+       for ``decorator`` to accept a list/tuple of decorator functions were
+       added.
 
 .. function:: decorator_from_middleware(middleware_class)
 

+ 3 - 2
docs/releases/1.9.txt

@@ -378,8 +378,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>`.
+* :func:`~django.utils.decorators.method_decorator` can now be used with a list
+  or tuple of decorators. It can also be used to :ref:`decorate classes instead
+  of methods <decorating-class-based-views>`.
 
 Internationalization
 ^^^^^^^^^^^^^^^^^^^^

+ 21 - 1
docs/topics/class-based-views/intro.txt

@@ -286,9 +286,29 @@ of the method to be decorated as the keyword argument ``name``::
     class ProtectedView(TemplateView):
         template_name = 'secret.html'
 
+If you have a set of common decorators used in several places, you can define
+a list or tuple of decorators and use this instead of invoking
+``method_decorator()`` multiple times. These two classes are equivalent::
+
+    decorators = [never_cache, login_required]
+
+    @method_decorator(decorators, name='dispatch')
+    class ProtectedView(TemplateView):
+        template_name = 'secret.html'
+
+    @method_decorator(never_cache, name='dispatch')
+    @method_decorator(login_required, name='dispatch')
+    class ProtectedView(TemplateView):
+        template_name = 'secret.html'
+
+The decorators will process a request in the order they are passed to the
+decorator. In the example, ``never_cache()`` will process the request before
+``login_required()``.
+
 .. versionchanged:: 1.9
 
-    The ability to use ``method_decorator()`` on a class was added.
+    The ability to use ``method_decorator()`` on a class and the ability for
+    it to accept a list or tuple of decorators were added.
 
 In this example, every instance of ``ProtectedView`` will have login protection.
 

+ 73 - 8
tests/decorators/tests.py

@@ -212,22 +212,52 @@ class MethodDecoratorTests(SimpleTestCase):
         self.assertEqual(getattr(func, 'myattr', False), True)
         self.assertEqual(getattr(func, 'myattr2', False), True)
 
-        # Now check method_decorator
-        class Test(object):
+        # Decorate using method_decorator() on the method.
+        class TestPlain(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)
+        # Decorate using method_decorator() on both the class and the method.
+        # The decorators applied to the methods are applied before the ones
+        # applied to the class.
+        @method_decorator(myattr_dec_m, "method")
+        class TestMethodAndClass(object):
+            @method_decorator(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)
+        # Decorate using an iterable of decorators.
+        decorators = (myattr_dec_m, myattr2_dec_m)
 
-        self.assertEqual(Test.method.__doc__, 'A method')
-        self.assertEqual(Test.method.__name__, 'method')
+        @method_decorator(decorators, "method")
+        class TestIterable(object):
+            def method(self):
+                "A method"
+                pass
+
+        for Test in (TestPlain, TestMethodAndClass, TestIterable):
+            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.__name__, 'method')
+
+    def test_bad_iterable(self):
+        decorators = {myattr_dec_m, myattr2_dec_m}
+        # The rest of the exception message differs between Python 2 and 3.
+        with self.assertRaisesMessage(TypeError, "'set' object"):
+            @method_decorator(decorators, "method")
+            class TestIterable(object):
+                def method(self):
+                    "A method"
+                    pass
 
     # Test for argumented decorator
     def test_argumented(self):
@@ -291,6 +321,41 @@ class MethodDecoratorTests(SimpleTestCase):
 
         self.assertTrue(Test().method())
 
+    def test_tuple_of_decorators(self):
+        """
+        @method_decorator can accept a tuple of decorators.
+        """
+        def add_question_mark(func):
+            def _wrapper(*args, **kwargs):
+                return func(*args, **kwargs) + "?"
+            return _wrapper
+
+        def add_exclamation_mark(func):
+            def _wrapper(*args, **kwargs):
+                return func(*args, **kwargs) + "!"
+            return _wrapper
+
+        # The order should be consistent with the usual order in which
+        # decorators are applied, e.g.
+        #    @add_exclamation_mark
+        #    @add_question_mark
+        #    def func():
+        #        ...
+        decorators = (add_exclamation_mark, add_question_mark)
+
+        @method_decorator(decorators, name="method")
+        class TestFirst(object):
+            def method(self):
+                return "hello world"
+
+        class TestSecond(object):
+            @method_decorator(decorators)
+            def method(self):
+                return "hello world"
+
+        self.assertEqual(TestFirst().method(), "hello world?!")
+        self.assertEqual(TestSecond().method(), "hello world?!")
+
     def test_invalid_non_callable_attribute_decoration(self):
         """
         @method_decorator on a non-callable attribute raises an error.