Ticket #14512: ticket14512.diff

File ticket14512.diff, 13.8 KB (added by Łukasz Rekucki, 14 years ago)

Cleaned up the patch a bit.

  • django/utils/decorators.py

    diff --git a/django/utils/decorators.py b/django/utils/decorators.py
    index d75d36f..afb011e 100644
    a b  
    11"Functions that help with dynamically creating decorators for views."
    22
    3 import types
    43try:
    54    from functools import wraps, update_wrapper, WRAPPER_ASSIGNMENTS
    65except ImportError:
    76    from django.utils.functional import wraps, update_wrapper, WRAPPER_ASSIGNMENTS  # Python 2.4 fallback.
    87
     8class classonlymethod(classmethod):
     9    def __get__(self, instance, owner):
     10        if instance is not None:
     11            raise AttributeError("This method is available only on the view class.")
     12        return super(classonlymethod, self).__get__(instance, owner)
    913
    1014def method_decorator(decorator):
    1115    """
    def method_decorator(decorator):  
    3943    return _dec
    4044
    4145
     46def view_decorator(fdec, subclass=False):
     47    """
     48    Change a function decorator into a view decorator.
     49
     50    This is a simplest approach possible. `as_view()` is replaced, so
     51    that it applies the given decorator before returning.
     52
     53    In this approach, decorators are always put on top - that means it's not
     54    possible to have functions called in this order:
     55
     56       B.dispatch, login_required, A.dispatch
     57
     58    NOTE: By default this modifies the given class, so be careful when doing this:
     59
     60       TemplateView = view_decorator(login_required)(TemplateView)
     61
     62    Because it will modify the TemplateView class. Instead create a fresh
     63    class first and apply the decorator there. A shortcut for this is
     64    specifying the ``subclass`` argument. But this is also dangerous. Consider:
     65
     66        @view_decorator(login_required, subclass=True)
     67        class MyView(View):
     68
     69            def get_context_data(self):
     70                data = super(MyView, self).get_context_data()
     71                data["foo"] = "bar"
     72                return data
     73
     74    This looks like a normal Python code, but there is a hidden infinite
     75    recursion, because of how `super()` works in Python 2.x; By the time
     76    `get_context_data()` is invoked, MyView refers to a subclass created in
     77    the decorator. super() looks at the next class in the MRO of MyView,
     78    which is the original MyView class we created, so it contains the
     79    `get_context_data()` method. Which is exactly the method that was just
     80    called. BOOM!
     81    """
     82    def decorator(cls):
     83        if subclass:
     84            cls = type("%sWithDecorator(%s)" % (cls.__name__, fdec.__name__), (cls,), {})
     85        @wraps(cls.as_view)
     86        def as_view(current, **initkwargs):
     87            return fdec(super(cls, current).as_view(**initkwargs))
     88        cls.as_view = classonlymethod(as_view)
     89        return cls
     90    return decorator
     91
     92
    4293def decorator_from_middleware_with_args(middleware_class):
    4394    """
    4495    Like decorator_from_middleware, but returns a function
  • django/views/generic/base.py

    diff --git a/django/views/generic/base.py b/django/views/generic/base.py
    index 19d0415..0c9cd79 100644
    a b  
    1 import copy
    21from django import http
    32from django.core.exceptions import ImproperlyConfigured
    43from django.template import RequestContext, loader
    5 from django.utils.translation import ugettext_lazy as _
    64from django.utils.functional import update_wrapper
    75from django.utils.log import getLogger
     6from django.utils.decorators import classonlymethod
    87
    98logger = getLogger('django.request')
    109
    11 class classonlymethod(classmethod):
    12     def __get__(self, instance, owner):
    13         if instance is not None:
    14             raise AttributeError("This method is available only on the view class.")
    15         return super(classonlymethod, self).__get__(instance, owner)
    16 
    1710class View(object):
    1811    """
    1912    Intentionally simple parent class for all views. Only implements
  • docs/topics/class-based-views.txt

    diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
    index f0e4910..56bc143 100644
    a b Because of the way that Python resolves method overloading, the local  
    537537:func:`render_to_response()` implementation will override the
    538538versions provided by :class:`JSONResponseMixin` and
    539539:class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
     540
     541Decorating
     542===========
     543
     544.. highlightlang:: python
     545
     546While class-based views and mixins introduce a whole new way of extending
     547view behaviour in a reusable way, you also use existing decorators.
     548
     549
     550Decorating in URLconf
     551-----------------------
     552
     553The simplest way of decorating class-based views is to decorate the result
     554of the :meth:`~django.views.generic.base.View.as_view` method. This
     555is most useful in the URLconf::
     556
     557    from django.views.generic import TemplateView
     558    from django.contrib.auth.decorators import login_required
     559
     560    urlpatterns = patterns('',
     561        (r'^about/',login_required(TemplateView.as_view(template_name="secret.html"))),
     562    )
     563
     564The downside of this approach is that you can't define a class-based view with
     565the decorator always applied to it.
     566
     567
     568Overriding ``as_view`` class method
     569--------------------------------------
     570
     571If you want your view to have a decorator applied and be able to subclass
     572it later, you need to override one of it's methods. The most straightforward
     573solution is to override :meth:`~django.views.generic.base.View.as_view`::
     574
     575    from django.views.generic import TemplateView
     576    from django.utils.decorators import classonlymethod
     577
     578    class ProtectedView(TemplateView):
     579
     580        @classonlymethod
     581        def as_view(cls, **initkwargs):
     582            return login_required(super(ProtectedView, cls).as_view(**initkwargs))
     583
     584This will make the ``ProtectedView`` and any of it's subclasses always apply the
     585``login_required`` decorator. The  ``classonlymethod`` decorator is a variation
     586of the standard Python ``classmethod`` decorator that lets you invoke the
     587method directly from the class, but not from an instance.
     588
     589
     590Class decorators
     591-----------------
     592
     593Django provides also provides a way to turn function decorators into
     594class decorators::
     595
     596    from django.utils.decorators import view_decorator
     597
     598    class ProtectedView(TemplateView):
     599        pass
     600    ProtectedView = view_decorator(login_required)(ProtectedView)
     601
     602In Python 2.6 and above you can also use class decorator syntax for this::
     603
     604    from django.utils.decorators import view_decorator
     605
     606    @view_decorator(login_required)
     607    class ProtectedView(TempalateView):
     608        pass
     609
     610.. note::
     611
     612    The produced class decorator modifies the class it was given, so it is
     613    *not safe* to decorate generic views this way **without creating a subclass**.
     614
     615    If you are absolutely sure you know how super() and MRO works in Python, you
     616    can pass a ``subclass`` keyword to make the decorator create an inline
     617    subclass for you::
     618
     619        ProtectedView = view_decorator(login_required, subclass=True)(TemplateView)
     620
     621    You have been warned!
  • tests/regressiontests/generic_views/base.py

    diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py
    index a1da986..d419acc 100644
    a b class SimplePostView(SimpleView):  
    2222class CustomizableView(SimpleView):
    2323    parameter = {}
    2424
     25
    2526def decorator(view):
    2627    view.is_decorated = True
    2728    return view
    class ViewTest(unittest.TestCase):  
    149150        """
    150151        self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
    151152
    152 
    153153class TemplateViewTest(TestCase):
    154154    urls = 'regressiontests.generic_views.urls'
    155155
  • tests/regressiontests/utils/decorators.py

    diff --git a/tests/regressiontests/utils/decorators.py b/tests/regressiontests/utils/decorators.py
    index ca9214f..fddc32e 100644
    a b  
    1 from django.test import TestCase
     1from django.test import TestCase, RequestFactory
     2from django.utils import unittest
     3from django.utils.decorators import method_decorator, wraps, view_decorator
     4from django.views.generic import View
     5
    26
    37class DecoratorFromMiddlewareTests(TestCase):
    48    """
    class DecoratorFromMiddlewareTests(TestCase):  
    1721        Test a middleware that implements process_view, operating on a callable class.
    1822        """
    1923        self.client.get('/utils/class_xview/')
     24
     25
     26def simple_dec(func):
     27    """
     28    Simple decotator for testing method_decorator and view_decorator.
     29    It assumes a request argument and one extra argument.  Appends
     30    string "decorator:" to the result. It also sets `is_decorated` attribute
     31    on the wrappper.
     32    """
     33    def wrapper(request, arg):
     34        return "decorator:" + func(request, arg)
     35    wrapper = wraps(func)(wrapper)
     36    wrapper.is_decorated = True
     37    return wrapper
     38
     39
     40class MethodDecoratorTests(TestCase):
     41    """
     42    Tests for method_decorator.
     43    """
     44
     45    def test_method_decorator(self):
     46        simple_dec_m = method_decorator(simple_dec)
     47
     48        class Test(object):
     49            @simple_dec_m
     50            def say(self, request, arg):
     51                return arg
     52
     53        self.assertEqual("decorator:hello", Test().say(None, "hello"))
     54        self.assertTrue(getattr(Test().say, "is_decorated", False),
     55                "Method decorator didn't preserve attributes.")
     56
     57
     58class ClassBasedViewDecorationTests(TestCase):
     59    rf = RequestFactory()
     60
     61    def test_decorate_view(self):
     62        class TextView(View):
     63            "Docstring"
     64            def get(self, request, text):
     65                return "get:" + text
     66            def post(self, request, text):
     67                return "post:" + text
     68        TextView = view_decorator(simple_dec)(TextView)
     69
     70        self.assertTrue(getattr(TextView.as_view(), "is_decorated", False),
     71                    "Class based view decorator didn't preserve attributes.")
     72        self.assertEqual(TextView.as_view().__doc__, "Docstring",
     73                    "Class based view decorator didn't preserve docstring.")
     74        self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"), "decorator:get:hello")
     75        self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"), "decorator:post:hello")
     76
     77    def test_super_calls(self):
     78        class TextView(View):
     79            def dispatch(self, request, text):
     80                return "view1:" + text
     81
     82        # NOTE: it's important for this test, that the definition
     83        # and decoration of the class happens in the *same scope*.
     84        class ViewWithSuper(TextView):
     85
     86            def __init__(self, **initargs):
     87                self.recursion_count = 0
     88                super(ViewWithSuper, self).__init__(**initargs)
     89
     90            def dispatch(self, *args, **kwargs):
     91                self.recursion_count += 1
     92                if self.recursion_count > 10:
     93                    raise Exception("Decoration caused recursive super() calls.")
     94                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
     95        ViewWithSuper = view_decorator(simple_dec)(ViewWithSuper)
     96
     97        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
     98
     99    @unittest.expectedFailure
     100    def test_super_calls_with_subclassing(self):
     101        class TextView(View):
     102            def dispatch(self, request, text):
     103                return "view1:" + text
     104
     105        # NOTE: it's important for this test, that the definition
     106        # and decoration of the class happens in the *same scope*.
     107        class ViewWithSuper(TextView):
     108
     109            def __init__(self, **initargs):
     110                self.recursion_count = 0
     111                super(ViewWithSuper, self).__init__(**initargs)
     112
     113            def dispatch(self, *args, **kwargs):
     114                self.recursion_count += 1
     115                if self.recursion_count > 10:
     116                    raise RuntimeError("Decoration caused recursive super() calls.")
     117                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
     118        ViewWithSuper = view_decorator(simple_dec, subclass=True)(ViewWithSuper)
     119
     120        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
     121
     122    def test_subclassing_decorated(self):
     123        """
     124        Test that decorators are always pushed to front.
     125        """
     126        class TextView(View):
     127            def dispatch(self, request, text):
     128                return "view1:" + text
     129        TextView = view_decorator(simple_dec)(TextView)
     130
     131        class SubView(TextView):
     132            def dispatch(self, *args, **kwargs):
     133                return "view2:" + super(SubView, self).dispatch(*args, **kwargs)
     134
     135        self.assertEqual(SubView.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
     136
     137    @unittest.expectedFailure
     138    def test_base_unmodified(self):
     139        class TextView(View):
     140            attr = "OK"
     141            def dispatch(self, request, text):
     142                return "view1:" + text
     143        DecoratedView = view_decorator(simple_dec)(TextView)
     144        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
     145        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
     146        self.assertFalse(DecoratedView is TextView)
     147        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])
     148
     149    def test_base_unmodified_with_subclassing(self):
     150        class TextView(View):
     151            attr = "OK"
     152            def dispatch(self, request, text):
     153                return "view1:" + text
     154        DecoratedView = view_decorator(simple_dec, subclass=True)(TextView)
     155
     156        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
     157        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
     158        self.assertFalse(DecoratedView is TextView)
     159        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])
Back to Top