Ticket #14512: ticket14512_draft.diff

File ticket14512_draft.diff, 15.0 KB (added by Łukasz Rekucki, 14 years ago)

Draft patch presenting 2 ways of decoration.

  • django/utils/decorators.py

    diff --git a/django/utils/decorators.py b/django/utils/decorators.py
    index d75d36f..58be3e8 100644
    a b def method_decorator(decorator):  
    3939    return _dec
    4040
    4141
     42def view_decorator(fdec, subclass=False):
     43    """
     44    Change a function decorator into a view decorator.
     45
     46    This is a simplest approach possible. `as_view()` is replaced, so
     47    that it applies the given decorator before returning.
     48
     49    In this approach, decorators are always put on top - that means it's not
     50    possible to have functions called in this order:
     51
     52       B.dispatch, login_required, A.dispatch
     53
     54    NOTE: By default this modifies the class it's called upon, so *MUST NOT* do:
     55
     56       TemplateView = view_decorator(login_required)(TemplateView)
     57
     58    Because it will modify the TemplateView class. Instead create a fresh
     59    class first and apply the decorator there. A shortcut for this is
     60    specifying the `subclass` argument. This is potentially dangerous. Consider:
     61
     62        @view_decorator(login_required, subclass=True)
     63        class MyView(View):
     64
     65            def get_context_data(self):
     66                data = super(MyView, self).get_context_data()
     67                data["foo"] = "bar"
     68                return data
     69
     70    This looks like a normal Python code, but there is a hidden infinite
     71    recursion, because of how `super()` works in Python 2.x; By the time
     72    `get_context_data()` is invoked, MyView refers to a subclass created in
     73    the decorator. super() looks at the next class in the MRO of MyView,
     74    which is the original MyView class we created, so it contains the
     75    `get_context_data()` method. Which is exactly the method that was just
     76    called. BOOM!
     77    """
     78
     79    from django.views.generic.base import classonlymethod
     80    def decorator(cls):
     81        if subclass:
     82            cls = type("%sWithDecorator(%s)" % (cls.__name__, fdec.__name__), (cls,), {})
     83        @wraps(cls.as_view.__func__)
     84        def as_view(current, **initkwargs):
     85            return fdec(super(cls, current).as_view(**initkwargs))
     86        cls.as_view = classonlymethod(as_view)
     87        return cls
     88    return decorator
     89
     90
    4291def decorator_from_middleware_with_args(middleware_class):
    4392    """
    4493    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..c1f4dd0 100644
    a b from django.template import RequestContext, loader  
    55from django.utils.translation import ugettext_lazy as _
    66from django.utils.functional import update_wrapper
    77from django.utils.log import getLogger
     8from itertools import chain
    89
    910logger = getLogger('django.request')
    1011
    class View(object):  
    2122    """
    2223
    2324    http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
     25    decorators = ()
    2426
    2527    def __init__(self, **kwargs):
    2628        """
    class View(object):  
    3234        for key, value in kwargs.iteritems():
    3335            setattr(self, key, value)
    3436
     37    @classmethod
     38    def get_decorators(cls):
     39        decorators = []
     40        # decorators in the subclass should be applied after the ones in base
     41        for klass in reversed(cls.__mro__):
     42            # decorators defined in the class should be applied in
     43            # the reverse order they're specified
     44            decorators.extend(reversed(klass.__dict__.get('decorators', ())))
     45        return decorators
     46
    3547    @classonlymethod
    3648    def as_view(cls, **initkwargs):
    3749        """
    class View(object):  
    5769        # and possible attributes set by decorators
    5870        # like csrf_exempt from dispatch
    5971        update_wrapper(view, cls.dispatch, assigned=())
     72
     73        # apply all defined decorators
     74        for decorator in cls.get_decorators():
     75            view = decorator(view)
     76
    6077        return view
    6178
    6279    def dispatch(self, request, *args, **kwargs):
  • docs/topics/class-based-views.txt

    diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
    index 821ded6..fe6c109 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
     549Simple decoration
     550------------------
     551
     552The simplest way of decorating class-based views is to decorate the result
     553of the :meth:`~django.views.generic.base.View.as_view` method. This
     554is most useful in the URLconf::
     555
     556    from django.views.generic import TemplateView
     557    from django.contrib.auth.decorators import login_required
     558
     559    urlpatterns = patterns('',
     560        (r'^about/',login_required(TemplateView.as_view(template_name="secret.html"))),
     561    )
     562
     563The downside of this approach is that you can't define a class-based view with
     564the decorator always applied to it.
     565
     566Overriding ``as_view`` class method
     567------------------------------------
     568
     569If you want your view to have a decorator applied and be able to subclass
     570it later, you need to override one of it's methods. The most straightforward
     571solution is to override :meth:`~django.views.generic.base.View.as_view`::
     572
     573    from django.views.generic import TemplateView, classonlymethod
     574
     575    class ProtectedView(TemplateView):
     576
     577        @classonlymethod
     578        def as_view(cls, **initkwargs):
     579            return login_required(super(ProtectedView, cls).as_view(**initkwargs))
     580
     581This will make the ``ProtectedView`` and any of it's subclasses always apply the
     582decorator. You don't need to worry about ``classonlymethod`` decorator.
     583It's not required for your view to work, but prevents from invoking
     584``as_view()`` on the view's instance by accident.
     585
     586
     587.. note::
     588
     589    This way of decorating (as the previous one) always applies the decorators
     590    at creation time, before any code in `dispatch()` has any chance to run.
     591
     592Class decorator
     593--------------------
     594
     595Python 2.6 introduces new syntax that allows you to decorate classes. Django
     596provides such a decorator for class-based views. The example above can
     597be simplified to::
     598
     599    from django.utils.decorators import view_decorator
     600
     601    @view_decorator(login_required)
     602    class ProtectedView(TemplateView)
     603        pass
     604
     605.. note::
     606
     607    The decorator modifies the class it was given, so it is **not safe** to decorate
     608    generic views this way without creating a subclass.
     609
     610    If you are absolutly sure you know how super() and MRO works in Python, you
     611    can pass ``subclass=True`` to the decorator and it will create a sublass
     612    for you. 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..1702d5b 100644
    a b from django.test import TestCase, RequestFactory  
    66from django.utils import simplejson
    77from django.views.generic import View, TemplateView, RedirectView
    88
     9def prepend_string(text):
     10    "Decorator that prepends strings to responses"
     11    from django.utils.functional import wraps
     12    def decorator(view):
     13        @wraps(view)
     14        def decorated_view(request, *args, **kwargs):
     15            return text + ':' + view(request, *args, **kwargs)
     16        return decorated_view
     17    return decorator
    918
    1019class SimpleView(View):
    1120    """
    class ViewTest(unittest.TestCase):  
    149158        """
    150159        self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
    151160
     161    def test_defining_decorators(self):
     162        """
     163        Test an alternate method of decoration.
     164        """
     165        class Base(View):
     166            def get(self, request, *args, **kwargs):
     167                return "Base"
     168
     169        class B(Base):
     170            decorators = (prepend_string('B'),)
     171           
     172        class AB(B):
     173            decorators = (prepend_string('A'),)
     174
     175        class CD(Base):
     176            # decorators are applied in reverse order
     177            decorators = (prepend_string('C'), prepend_string('D'))
     178
     179        class ABCD(AB, CD):
     180            pass
     181
     182        self.assertEqual(Base.as_view()(self.rf.get('/')), "Base")
     183        self.assertEqual(B.as_view()(self.rf.get('/')), "B:Base")
     184        self.assertEqual(AB.as_view()(self.rf.get('/')), "A:B:Base")
     185        self.assertEqual(CD.as_view()(self.rf.get('/')), "C:D:Base")
     186        self.assertEqual(ABCD.as_view()(self.rf.get('/')), "A:B:C:D:Base")
    152187
    153188class TemplateViewTest(TestCase):
    154189    urls = 'regressiontests.generic_views.urls'
  • tests/regressiontests/utils/decorators.py

    diff --git a/tests/regressiontests/utils/decorators.py b/tests/regressiontests/utils/decorators.py
    index ca9214f..74a29da 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, subclassing=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