Fixed #25269 -- Allowed method_decorator() to accept a list/tuple of decorators.
This commit is contained in:
parent
d8d853378b
commit
186eb21dc1
|
@ -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__
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue