summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRigel Di Scala <rigel.discala@propylon.com>2015-07-21 21:54:37 +0100
committerTim Graham <timograham@gmail.com>2015-08-01 08:38:03 -0400
commit3bdaaf6777804d33ee46cdb5a889b8cc544a91f1 (patch)
tree86399ce870c06f59584c98bc3988d9ed8c739c87
parent1a76257b1b385ac8afd67bd36d061f508613e4d2 (diff)
Fixed #25146 -- Allowed method_decorator() to decorate classes.
-rw-r--r--django/utils/decorators.py33
-rw-r--r--docs/ref/utils.txt10
-rw-r--r--docs/releases/1.9.txt3
-rw-r--r--docs/topics/class-based-views/intro.txt14
-rw-r--r--tests/decorators/tests.py51
5 files changed, 102 insertions, 9 deletions
diff --git a/django/utils/decorators.py b/django/utils/decorators.py
index 1ea12b32d2..294761672c 100644
--- a/django/utils/decorators.py
+++ b/django/utils/decorators.py
@@ -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))
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index c9deba887a..12bb351df0 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -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
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 82ff5a08bc..f016a08c32 100644
--- a/docs/releases/1.9.txt
+++ b/docs/releases/1.9.txt
@@ -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
^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/topics/class-based-views/intro.txt b/docs/topics/class-based-views/intro.txt
index 6724bec0da..2e5cc1bca1 100644
--- a/docs/topics/class-based-views/intro.txt
+++ b/docs/topics/class-based-views/intro.txt
@@ -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::
diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py
index d6b2c78068..0f0f0f52c2 100644
--- a/tests/decorators/tests.py
+++ b/tests/decorators/tests.py
@@ -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):
"""