Ticket #14512: ticket14512.diff
File ticket14512.diff, 13.8 KB (added by , 14 years ago) |
---|
-
django/utils/decorators.py
diff --git a/django/utils/decorators.py b/django/utils/decorators.py index d75d36f..afb011e 100644
a b 1 1 "Functions that help with dynamically creating decorators for views." 2 2 3 import types4 3 try: 5 4 from functools import wraps, update_wrapper, WRAPPER_ASSIGNMENTS 6 5 except ImportError: 7 6 from django.utils.functional import wraps, update_wrapper, WRAPPER_ASSIGNMENTS # Python 2.4 fallback. 8 7 8 class 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) 9 13 10 14 def method_decorator(decorator): 11 15 """ … … def method_decorator(decorator): 39 43 return _dec 40 44 41 45 46 def 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 42 93 def decorator_from_middleware_with_args(middleware_class): 43 94 """ 44 95 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 copy2 1 from django import http 3 2 from django.core.exceptions import ImproperlyConfigured 4 3 from django.template import RequestContext, loader 5 from django.utils.translation import ugettext_lazy as _6 4 from django.utils.functional import update_wrapper 7 5 from django.utils.log import getLogger 6 from django.utils.decorators import classonlymethod 8 7 9 8 logger = getLogger('django.request') 10 9 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 17 10 class View(object): 18 11 """ 19 12 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 537 537 :func:`render_to_response()` implementation will override the 538 538 versions provided by :class:`JSONResponseMixin` and 539 539 :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. 540 541 Decorating 542 =========== 543 544 .. highlightlang:: python 545 546 While class-based views and mixins introduce a whole new way of extending 547 view behaviour in a reusable way, you also use existing decorators. 548 549 550 Decorating in URLconf 551 ----------------------- 552 553 The simplest way of decorating class-based views is to decorate the result 554 of the :meth:`~django.views.generic.base.View.as_view` method. This 555 is 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 564 The downside of this approach is that you can't define a class-based view with 565 the decorator always applied to it. 566 567 568 Overriding ``as_view`` class method 569 -------------------------------------- 570 571 If you want your view to have a decorator applied and be able to subclass 572 it later, you need to override one of it's methods. The most straightforward 573 solution 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 584 This will make the ``ProtectedView`` and any of it's subclasses always apply the 585 ``login_required`` decorator. The ``classonlymethod`` decorator is a variation 586 of the standard Python ``classmethod`` decorator that lets you invoke the 587 method directly from the class, but not from an instance. 588 589 590 Class decorators 591 ----------------- 592 593 Django provides also provides a way to turn function decorators into 594 class 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 602 In 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): 22 22 class CustomizableView(SimpleView): 23 23 parameter = {} 24 24 25 25 26 def decorator(view): 26 27 view.is_decorated = True 27 28 return view … … class ViewTest(unittest.TestCase): 149 150 """ 150 151 self.assertTrue(DecoratedDispatchView.as_view().is_decorated) 151 152 152 153 153 class TemplateViewTest(TestCase): 154 154 urls = 'regressiontests.generic_views.urls' 155 155 -
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 1 from django.test import TestCase, RequestFactory 2 from django.utils import unittest 3 from django.utils.decorators import method_decorator, wraps, view_decorator 4 from django.views.generic import View 5 2 6 3 7 class DecoratorFromMiddlewareTests(TestCase): 4 8 """ … … class DecoratorFromMiddlewareTests(TestCase): 17 21 Test a middleware that implements process_view, operating on a callable class. 18 22 """ 19 23 self.client.get('/utils/class_xview/') 24 25 26 def 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 40 class 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 58 class 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])