summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorfabrizio ettore messina <fabrizio.messina@mistralpay.com>2015-08-11 13:35:50 +0200
committerTim Graham <timograham@gmail.com>2015-09-18 19:04:29 -0400
commit186eb21dc159807dba83148f7c9c50d470745708 (patch)
treeb906d98d94bde119bef7ee542cb842e91f1c51ab
parentd8d853378b3ff75c03d8bd91ea026d2b8c642b0f (diff)
Fixed #25269 -- Allowed method_decorator() to accept a list/tuple of decorators.
-rw-r--r--django/utils/decorators.py22
-rw-r--r--docs/ref/utils.txt14
-rw-r--r--docs/releases/1.9.txt5
-rw-r--r--docs/topics/class-based-views/intro.txt22
-rw-r--r--tests/decorators/tests.py81
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<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)
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 <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
^^^^^^^^^^^^^^^^^^^^
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.