Code

Ticket #14512: ticket14512_draft.diff

File ticket14512_draft.diff, 15.0 KB (added by lrekucki, 4 years ago)

Draft patch presenting 2 ways of decoration.

Line 
1diff --git a/django/utils/decorators.py b/django/utils/decorators.py
2index d75d36f..58be3e8 100644
3--- a/django/utils/decorators.py
4+++ b/django/utils/decorators.py
5@@ -39,6 +39,55 @@ def method_decorator(decorator):
6     return _dec
7 
8 
9+def view_decorator(fdec, subclass=False):
10+    """
11+    Change a function decorator into a view decorator.
12+
13+    This is a simplest approach possible. `as_view()` is replaced, so
14+    that it applies the given decorator before returning.
15+
16+    In this approach, decorators are always put on top - that means it's not
17+    possible to have functions called in this order:
18+
19+       B.dispatch, login_required, A.dispatch
20+
21+    NOTE: By default this modifies the class it's called upon, so *MUST NOT* do:
22+
23+       TemplateView = view_decorator(login_required)(TemplateView)
24+
25+    Because it will modify the TemplateView class. Instead create a fresh
26+    class first and apply the decorator there. A shortcut for this is
27+    specifying the `subclass` argument. This is potentially dangerous. Consider:
28+
29+        @view_decorator(login_required, subclass=True)
30+        class MyView(View):
31+
32+            def get_context_data(self):
33+                data = super(MyView, self).get_context_data()
34+                data["foo"] = "bar"
35+                return data
36+
37+    This looks like a normal Python code, but there is a hidden infinite
38+    recursion, because of how `super()` works in Python 2.x; By the time
39+    `get_context_data()` is invoked, MyView refers to a subclass created in
40+    the decorator. super() looks at the next class in the MRO of MyView,
41+    which is the original MyView class we created, so it contains the
42+    `get_context_data()` method. Which is exactly the method that was just
43+    called. BOOM!
44+    """
45+
46+    from django.views.generic.base import classonlymethod
47+    def decorator(cls):
48+        if subclass:
49+            cls = type("%sWithDecorator(%s)" % (cls.__name__, fdec.__name__), (cls,), {})
50+        @wraps(cls.as_view.__func__)
51+        def as_view(current, **initkwargs):
52+            return fdec(super(cls, current).as_view(**initkwargs))
53+        cls.as_view = classonlymethod(as_view)
54+        return cls
55+    return decorator
56+
57+
58 def decorator_from_middleware_with_args(middleware_class):
59     """
60     Like decorator_from_middleware, but returns a function
61diff --git a/django/views/generic/base.py b/django/views/generic/base.py
62index 19d0415..c1f4dd0 100644
63--- a/django/views/generic/base.py
64+++ b/django/views/generic/base.py
65@@ -5,6 +5,7 @@ from django.template import RequestContext, loader
66 from django.utils.translation import ugettext_lazy as _
67 from django.utils.functional import update_wrapper
68 from django.utils.log import getLogger
69+from itertools import chain
70 
71 logger = getLogger('django.request')
72 
73@@ -21,6 +22,7 @@ class View(object):
74     """
75 
76     http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace']
77+    decorators = ()
78 
79     def __init__(self, **kwargs):
80         """
81@@ -32,6 +34,16 @@ class View(object):
82         for key, value in kwargs.iteritems():
83             setattr(self, key, value)
84 
85+    @classmethod
86+    def get_decorators(cls):
87+        decorators = []
88+        # decorators in the subclass should be applied after the ones in base
89+        for klass in reversed(cls.__mro__):
90+            # decorators defined in the class should be applied in
91+            # the reverse order they're specified
92+            decorators.extend(reversed(klass.__dict__.get('decorators', ())))
93+        return decorators
94+
95     @classonlymethod
96     def as_view(cls, **initkwargs):
97         """
98@@ -57,6 +69,11 @@ class View(object):
99         # and possible attributes set by decorators
100         # like csrf_exempt from dispatch
101         update_wrapper(view, cls.dispatch, assigned=())
102+
103+        # apply all defined decorators
104+        for decorator in cls.get_decorators():
105+            view = decorator(view)
106+
107         return view
108 
109     def dispatch(self, request, *args, **kwargs):
110diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt
111index 821ded6..fe6c109 100644
112--- a/docs/topics/class-based-views.txt
113+++ b/docs/topics/class-based-views.txt
114@@ -537,3 +537,76 @@ Because of the way that Python resolves method overloading, the local
115 :func:`render_to_response()` implementation will override the
116 versions provided by :class:`JSONResponseMixin` and
117 :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`.
118+
119+Decorating
120+===========
121+
122+.. highlightlang:: python
123+
124+While class-based views and mixins introduce a whole new way of extending
125+view behaviour in a reusable way, you also use existing decorators.
126+
127+Simple decoration
128+------------------
129+
130+The simplest way of decorating class-based views is to decorate the result
131+of the :meth:`~django.views.generic.base.View.as_view` method. This
132+is most useful in the URLconf::
133+
134+    from django.views.generic import TemplateView
135+    from django.contrib.auth.decorators import login_required
136+
137+    urlpatterns = patterns('',
138+        (r'^about/',login_required(TemplateView.as_view(template_name="secret.html"))),
139+    )
140+
141+The downside of this approach is that you can't define a class-based view with
142+the decorator always applied to it.
143+
144+Overriding ``as_view`` class method
145+------------------------------------
146+
147+If you want your view to have a decorator applied and be able to subclass
148+it later, you need to override one of it's methods. The most straightforward
149+solution is to override :meth:`~django.views.generic.base.View.as_view`::
150+
151+    from django.views.generic import TemplateView, classonlymethod
152+
153+    class ProtectedView(TemplateView):
154+
155+        @classonlymethod
156+        def as_view(cls, **initkwargs):
157+            return login_required(super(ProtectedView, cls).as_view(**initkwargs))
158+
159+This will make the ``ProtectedView`` and any of it's subclasses always apply the
160+decorator. You don't need to worry about ``classonlymethod`` decorator.
161+It's not required for your view to work, but prevents from invoking
162+``as_view()`` on the view's instance by accident.
163+
164+
165+.. note::
166+
167+    This way of decorating (as the previous one) always applies the decorators
168+    at creation time, before any code in `dispatch()` has any chance to run.
169+
170+Class decorator
171+--------------------
172+
173+Python 2.6 introduces new syntax that allows you to decorate classes. Django
174+provides such a decorator for class-based views. The example above can
175+be simplified to::
176+
177+    from django.utils.decorators import view_decorator
178+
179+    @view_decorator(login_required)
180+    class ProtectedView(TemplateView)
181+        pass
182+
183+.. note::
184+
185+    The decorator modifies the class it was given, so it is **not safe** to decorate
186+    generic views this way without creating a subclass.
187+
188+    If you are absolutly sure you know how super() and MRO works in Python, you
189+    can pass ``subclass=True`` to the decorator and it will create a sublass
190+    for you. You have been warned!
191diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py
192index a1da986..1702d5b 100644
193--- a/tests/regressiontests/generic_views/base.py
194+++ b/tests/regressiontests/generic_views/base.py
195@@ -6,6 +6,15 @@ from django.test import TestCase, RequestFactory
196 from django.utils import simplejson
197 from django.views.generic import View, TemplateView, RedirectView
198 
199+def prepend_string(text):
200+    "Decorator that prepends strings to responses"
201+    from django.utils.functional import wraps
202+    def decorator(view):
203+        @wraps(view)
204+        def decorated_view(request, *args, **kwargs):
205+            return text + ':' + view(request, *args, **kwargs)
206+        return decorated_view
207+    return decorator
208 
209 class SimpleView(View):
210     """
211@@ -149,6 +158,32 @@ class ViewTest(unittest.TestCase):
212         """
213         self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
214 
215+    def test_defining_decorators(self):
216+        """
217+        Test an alternate method of decoration.
218+        """
219+        class Base(View):
220+            def get(self, request, *args, **kwargs):
221+                return "Base"
222+
223+        class B(Base):
224+            decorators = (prepend_string('B'),)
225+           
226+        class AB(B):
227+            decorators = (prepend_string('A'),)
228+
229+        class CD(Base):
230+            # decorators are applied in reverse order
231+            decorators = (prepend_string('C'), prepend_string('D'))
232+
233+        class ABCD(AB, CD):
234+            pass
235+
236+        self.assertEqual(Base.as_view()(self.rf.get('/')), "Base")
237+        self.assertEqual(B.as_view()(self.rf.get('/')), "B:Base")
238+        self.assertEqual(AB.as_view()(self.rf.get('/')), "A:B:Base")
239+        self.assertEqual(CD.as_view()(self.rf.get('/')), "C:D:Base")
240+        self.assertEqual(ABCD.as_view()(self.rf.get('/')), "A:B:C:D:Base")
241 
242 class TemplateViewTest(TestCase):
243     urls = 'regressiontests.generic_views.urls'
244diff --git a/tests/regressiontests/utils/decorators.py b/tests/regressiontests/utils/decorators.py
245index ca9214f..74a29da 100644
246--- a/tests/regressiontests/utils/decorators.py
247+++ b/tests/regressiontests/utils/decorators.py
248@@ -1,4 +1,8 @@
249-from django.test import TestCase
250+from django.test import TestCase, RequestFactory
251+from django.utils import unittest
252+from django.utils.decorators import method_decorator, wraps, view_decorator
253+from django.views.generic import View
254+
255 
256 class DecoratorFromMiddlewareTests(TestCase):
257     """
258@@ -17,3 +21,139 @@ class DecoratorFromMiddlewareTests(TestCase):
259         Test a middleware that implements process_view, operating on a callable class.
260         """
261         self.client.get('/utils/class_xview/')
262+
263+
264+def simple_dec(func):
265+    """
266+    Simple decotator for testing method_decorator and view_decorator.
267+    It assumes a request argument and one extra argument.  Appends
268+    string "decorator:" to the result. It also sets `is_decorated` attribute
269+    on the wrappper.
270+    """
271+    def wrapper(request, arg):
272+        return "decorator:" + func(request, arg)
273+    wrapper = wraps(func)(wrapper)
274+    wrapper.is_decorated = True
275+    return wrapper
276+
277+
278+class MethodDecoratorTests(TestCase):
279+    """
280+    Tests for method_decorator.
281+    """
282+
283+    def test_method_decorator(self):
284+        simple_dec_m = method_decorator(simple_dec)
285+
286+        class Test(object):
287+            @simple_dec_m
288+            def say(self, request, arg):
289+                return arg
290+
291+        self.assertEqual("decorator:hello", Test().say(None, "hello"))
292+        self.assertTrue(getattr(Test().say, "is_decorated", False),
293+                "Method decorator didn't preserve attributes.")
294+
295+
296+class ClassBasedViewDecorationTests(TestCase):
297+    rf = RequestFactory()
298+
299+    def test_decorate_view(self):
300+        class TextView(View):
301+            "Docstring"
302+            def get(self, request, text):
303+                return "get:" + text
304+            def post(self, request, text):
305+                return "post:" + text
306+        TextView = view_decorator(simple_dec)(TextView)
307+
308+        self.assertTrue(getattr(TextView.as_view(), "is_decorated", False),
309+                    "Class based view decorator didn't preserve attributes.")
310+        self.assertEqual(TextView.as_view().__doc__, "Docstring",
311+                    "Class based view decorator didn't preserve docstring.")
312+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "hello"), "decorator:get:hello")
313+        self.assertEqual(TextView.as_view()(self.rf.post('/'), "hello"), "decorator:post:hello")
314+
315+    def test_super_calls(self):
316+        class TextView(View):
317+            def dispatch(self, request, text):
318+                return "view1:" + text
319+
320+        # NOTE: it's important for this test, that the definition
321+        # and decoration of the class happens in the *same scope*.
322+        class ViewWithSuper(TextView):
323+
324+            def __init__(self, **initargs):
325+                self.recursion_count = 0
326+                super(ViewWithSuper, self).__init__(**initargs)
327+
328+            def dispatch(self, *args, **kwargs):
329+                self.recursion_count += 1
330+                if self.recursion_count > 10:
331+                    raise Exception("Decoration caused recursive super() calls.")
332+                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
333+        ViewWithSuper = view_decorator(simple_dec)(ViewWithSuper)
334+
335+        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
336+
337+    @unittest.expectedFailure
338+    def test_super_calls_with_subclassing(self):
339+        class TextView(View):
340+            def dispatch(self, request, text):
341+                return "view1:" + text
342+
343+        # NOTE: it's important for this test, that the definition
344+        # and decoration of the class happens in the *same scope*.
345+        class ViewWithSuper(TextView):
346+
347+            def __init__(self, **initargs):
348+                self.recursion_count = 0
349+                super(ViewWithSuper, self).__init__(**initargs)
350+
351+            def dispatch(self, *args, **kwargs):
352+                self.recursion_count += 1
353+                if self.recursion_count > 10:
354+                    raise RuntimeError("Decoration caused recursive super() calls.")
355+                return "view2:" + super(ViewWithSuper, self).dispatch(*args, **kwargs)
356+        ViewWithSuper = view_decorator(simple_dec, subclassing=True)(ViewWithSuper)
357+
358+        self.assertEqual(ViewWithSuper.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
359+
360+    def test_subclassing_decorated(self):
361+        """
362+        Test that decorators are always pushed to front.
363+        """
364+        class TextView(View):
365+            def dispatch(self, request, text):
366+                return "view1:" + text
367+        TextView = view_decorator(simple_dec)(TextView)
368+
369+        class SubView(TextView):
370+            def dispatch(self, *args, **kwargs):
371+                return "view2:" + super(SubView, self).dispatch(*args, **kwargs)
372+
373+        self.assertEqual(SubView.as_view()(self.rf.get('/'), "A"), "decorator:view2:view1:A")
374+
375+    @unittest.expectedFailure
376+    def test_base_unmodified(self):
377+        class TextView(View):
378+            attr = "OK"
379+            def dispatch(self, request, text):
380+                return "view1:" + text
381+        DecoratedView = view_decorator(simple_dec)(TextView)
382+        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
383+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
384+        self.assertFalse(DecoratedView is TextView)
385+        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])
386+
387+    def test_base_unmodified_with_subclassing(self):
388+        class TextView(View):
389+            attr = "OK"
390+            def dispatch(self, request, text):
391+                return "view1:" + text
392+        DecoratedView = view_decorator(simple_dec, subclass=True)(TextView)
393+
394+        self.assertEqual(DecoratedView.as_view()(self.rf.get('/'), "A"), "decorator:view1:A")
395+        self.assertEqual(TextView.as_view()(self.rf.get('/'), "A"), "view1:A")
396+        self.assertFalse(DecoratedView is TextView)
397+        self.assertEqual(DecoratedView.mro(), [DecoratedView, TextView, View, object])