From 186eb21dc159807dba83148f7c9c50d470745708 Mon Sep 17 00:00:00 2001 From: fabrizio ettore messina Date: Tue, 11 Aug 2015 13:35:50 +0200 Subject: [PATCH] Fixed #25269 -- Allowed method_decorator() to accept a list/tuple of decorators. --- django/utils/decorators.py | 22 +++++-- docs/ref/utils.txt | 14 ++++- docs/releases/1.9.txt | 5 +- docs/topics/class-based-views/intro.txt | 22 ++++++- tests/decorators/tests.py | 81 ++++++++++++++++++++++--- 5 files changed, 126 insertions(+), 18 deletions(-) diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 294761672c..4e2d0a179e 100644 --- a/django/utils/decorators.py +++ b/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__ diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index c1df458872..c24551b9ec 100644 --- a/docs/ref/utils.txt +++ b/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` 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 ` 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) diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index c2079f27d0..3f101a5f94 100644 --- a/docs/releases/1.9.txt +++ b/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 `. +* :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 `. Internationalization ^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt index 0b4a02cfbb..5e3351e90c 100644 --- a/docs/topics/class-based-views/intro.txt +++ b/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. diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index 0f0f0f52c2..3414264f72 100644 --- a/tests/decorators/tests.py +++ b/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.