Fixed #25146 -- Allowed method_decorator() to decorate classes.

This commit is contained in:
Rigel Di Scala 2015-07-21 21:54:37 +01:00 committed by Tim Graham
parent 1a76257b1b
commit 3bdaaf6777
5 changed files with 102 additions and 9 deletions

View File

@ -17,13 +17,34 @@ class classonlymethod(classmethod):
return super(classonlymethod, self).__get__(instance, owner)
def method_decorator(decorator):
def method_decorator(decorator, name=''):
"""
Converts a function decorator into a method decorator
"""
# 'func' is a function at the time it is passed to _dec, but will eventually
# be a method of the class it is defined on.
def _dec(func):
# 'obj' can be a class or a function. If 'obj' is a function at the time it
# is passed to _dec, it will eventually be a method of the class it is
# defined on. If 'obj' is a class, the 'name' is required to be the name
# of the method that will be decorated.
def _dec(obj):
is_class = isinstance(obj, type)
if is_class:
if name and hasattr(obj, name):
func = getattr(obj, name)
if not callable(func):
raise TypeError(
"Cannot decorate '{0}' as it isn't a callable "
"attribute of {1} ({2})".format(name, obj, func)
)
else:
raise ValueError(
"The keyword argument `name` must be the name of a method "
"of the decorated class: {0}. Got '{1}' instead".format(
obj, name,
)
)
else:
func = obj
def _wrapper(self, *args, **kwargs):
@decorator
def bound_func(*args2, **kwargs2):
@ -43,6 +64,10 @@ def method_decorator(decorator):
# Need to preserve any existing attributes of 'func', including the name.
update_wrapper(_wrapper, func)
if is_class:
setattr(obj, name, _wrapper)
return obj
return _wrapper
update_wrapper(_dec, decorator, assigned=available_attrs(decorator))

View File

@ -151,11 +151,17 @@ The functions defined in this module share the following properties:
.. module:: django.utils.decorators
:synopsis: Functions that help with creating decorators for views.
.. function:: method_decorator(decorator)
.. function:: method_decorator(decorator, name='')
Converts a function decorator into a method decorator. See :ref:`decorating
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.
.. versionchanged:: 1.9
The ability to decorate classes and the ``name`` parameter were added.
.. function:: decorator_from_middleware(middleware_class)
Given a middleware class, returns a view decorator. This lets you use

View File

@ -338,6 +338,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>`.
Internationalization
^^^^^^^^^^^^^^^^^^^^

View File

@ -279,8 +279,18 @@ that it can be used on an instance method. For example::
def dispatch(self, *args, **kwargs):
return super(ProtectedView, self).dispatch(*args, **kwargs)
In this example, every instance of ``ProtectedView`` will have
login protection.
Or, more succinctly, you can decorate the class instead and pass the name
of the method to be decorated as the keyword argument ``name``::
@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
template_name = 'secret.html'
.. versionchanged:: 1.9
The ability to use ``method_decorator()`` on a class was added.
In this example, every instance of ``ProtectedView`` will have login protection.
.. note::

View File

@ -7,6 +7,7 @@ from django.contrib.auth.decorators import (
)
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed
from django.middleware.clickjacking import XFrameOptionsMiddleware
from django.test import SimpleTestCase
from django.utils.decorators import method_decorator
from django.utils.functional import allow_lazy, lazy
from django.views.decorators.cache import (
@ -189,7 +190,7 @@ class ClsDec(object):
return update_wrapper(wrapped, f)
class MethodDecoratorTests(TestCase):
class MethodDecoratorTests(SimpleTestCase):
"""
Tests for method_decorator
"""
@ -274,6 +275,54 @@ class MethodDecoratorTests(TestCase):
self.assertEqual(Test().method(1), 1)
def test_class_decoration(self):
"""
@method_decorator can be used to decorate a class and its methods.
"""
def deco(func):
def _wrapper(*args, **kwargs):
return True
return _wrapper
@method_decorator(deco, name="method")
class Test(object):
def method(self):
return False
self.assertTrue(Test().method())
def test_invalid_non_callable_attribute_decoration(self):
"""
@method_decorator on a non-callable attribute raises an error.
"""
msg = (
"Cannot decorate 'prop' as it isn't a callable attribute of "
"<class 'Test'> (1)"
)
with self.assertRaisesMessage(TypeError, msg):
@method_decorator(lambda: None, name="prop")
class Test(object):
prop = 1
@classmethod
def __module__(cls):
return "tests"
def test_invalid_method_name_to_decorate(self):
"""
@method_decorator on a nonexistent method raises an error.
"""
msg = (
"The keyword argument `name` must be the name of a method of the "
"decorated class: <class 'Test'>. Got 'non_existing_method' instead"
)
with self.assertRaisesMessage(ValueError, msg):
@method_decorator(lambda: None, name="non_existing_method")
class Test(object):
@classmethod
def __module__(cls):
return "tests"
class XFrameOptionsDecoratorsTests(TestCase):
"""