Commit 3bdaaf67 authored by Rigel Di Scala's avatar Rigel Di Scala Committed by Tim Graham
Browse files

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

parent 1a76257b
Loading
Loading
Loading
Loading
+29 −4
Original line number Diff line number Diff line
@@ -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))
+8 −2
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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
^^^^^^^^^^^^^^^^^^^^

+12 −2
Original line number Diff line number Diff line
@@ -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::

+50 −1
Original line number Diff line number Diff line
@@ -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):
    """