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

This commit is contained in:
fabrizio ettore messina 2015-08-11 13:35:50 +02:00 committed by Tim Graham
parent d8d853378b
commit 186eb21dc1
5 changed files with 126 additions and 18 deletions

View File

@ -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__

View File

@ -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)

View File

@ -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
^^^^^^^^^^^^^^^^^^^^

View File

@ -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.

View File

@ -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.