Attempting to make decorators anticipate every usage is the wrong approach. 
In Django, the sole interface to a view is as a callable that accepts an 
`HttpRequest` and turns it into an `HttpResponse` or exception. Django the 
framework doesn't actually need to support class-based views--because CBVs 
are just another way to build view callables.

View callables are an elegant abstraction that keeps the core of Django, 
and the decorators it provides, simple. Trying to anticipate all the ways 
of building view callables is not only complex, it is impossible. Why 
shouldn't I be able to use the built-in decorators with my own 
implementations of class-based views? If the built-in decorators' 
implementations are simple, I will be able to provide an adapter to use 
them with my CBV system. There are many useful decorators built-in to 
Django and in the wild, and my adapter will work with all of them, because 
they treat views as simple callables.

`method_decorator` is the right idea, but it is painful to use. It requires 
a tedious empty `dispatch` method, just to have something to decorate. 
Using decorators to modify views makes sense, because decoration is the 
only way to modify the functionality of a callable. Classes, however, have 
much more powerful ways of adding optional functionality: multiple 
inheritance.


# `DecoratorMixin`

`DecoratorMixin` is a class factory that converts any view decorator into a 
class-based view mixin. It works as you would expect with inheritance--MRO 
provides the same ordering that function decorators do, and multiple 
inheritance allows the mixins to be composed naturally. Inheriting from a 
decorated view decorates the subclass as you would expect. It averts the 
need for the annoying empty `dispatch` method in each class that wants to 
use a decorator.

Its implementation is simple: https://gist.github.com/gavinwahl/5694349. 
This is a complete implementation, and is all that's necessary to use view 
decorators with Django's CBVs. I use this code in all my projects with 
great success. [Here is an 
example](https://github.com/fusionbox/django-authtools/blob/master/authtools/views.py#L94)
 
of its use in a reimplementation of Django's auth views as CBVs.

Using it is easy and intuitive:

    LoginRequiredMixin = DecoratorMixin(login_required)
 
    class LoginRequiredAndCsrfMixin(DecoratorMixin(login_required),
                                    DecoratorMixin(csrf_protect)):
        """
        Inheriting from this class is the same as decorating your view 
thus::
        
            @login_required
            @csrf_protect
            def someview(request):
                pass
        """
        pass
        
    class SomeView(LoginRequiredMixin, View):
    # ...
               
               
I propose `DecoratorMixin` be added to Django. It retains compatibility 
with all existing decorators, elegantly adapts functional decorators to 
good object-oriented practices, and has a simple implementation. 
'Universal' decorators don't accommodate existing third-party code, contain 
complex magic, and annul one of the most elegant principles of Django: 
views are simply callables.


On Thursday, September 15, 2011 2:44:39 PM UTC-6, Jacob Kaplan-Moss wrote:
>
> Hi folks --
>
> I'd like to convert all the view decorators built into Django to be
> "universal" -- so they'll work to decorate *any* view, whether a
> function, method, or class. I believe I've figured out a technique for
> this, but I'd like some feedback on the approach before I dive in too
> deep.
>
> Right now view decorators (e.g. @login_required) only work on
> functions. If you want to use a decorator on a method then you need to
> "convert" the decorator using method_decorator(original_decorator).
> You can't use view decorators on class-based views at all. This means
> making a class-based view require login requires this awesomeness::
>
>     class MyView(View):
>         @method_decorator(login_required)
>         def dispatch(self, *args, **kwargs):
>             return super(MyView, self.dispatch(*args, **kwargs)
>
> This makes me sad. It's really counter-intuitive and relies on a
> recognizing that functions and methods are different to even know to
> look for method_decorator.
>
> #14512 proposes a adding another view-decorator-factory for decorating
> class-based views, which would turn the above into::
>
>     @class_view_decorator(login_required)
>     class MyView(View):
>         ...
>
> This makes me less sad, but still sad. Factory functions. Ugh.
>
> I want @login_required to work for anything::
>
>     @login_required
>     class MyView(View):
>         ...
>
>     class Something(object):
>         @login_required
>         def my_view(self, request):
>             ...
>
>     @login_required
>     def my_view(request):
>         ...
>
>
> Now, back in the day we somewhat had this: decorators tried to work
> with both functions and methods. The technique turned out not to work
> (see #12804) and was removed in [12399]. See
>
> http://groups.google.com/group/django-developers/browse_thread/thread/3024b14a47f5404d
> for the discussion.
>
> I believe, however, I've figured out a different technique to make
> this work: don't try to detect bound versus unbound methods, but
> instead look for the HttpRequest object. It'll either be args[0] if
> the view's a function, or args[1] if the view's a method. This
> technique won't work for any old decorator, but it *will* work (I
> think) for any decorator *designed to be applied only to views*.
>
> I've written a proof-of-concept patch to @login_required (well,
> @user_passes_test, actually):
>
>     https://gist.github.com/1220375
>
> The test suite passes with this, with one exception:
>
> https://code.djangoproject.com/browser/django/trunk/tests/regressiontests/decorators/tests.py#L87
> .
> I maintain that this test is broken and should be using RequestFactory
> instead.
>
> Can I get some thoughts on this technique and some feedback on whether
> it's OK to apply to every decorator built into Django?
>
> Jacob
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-developers+unsubscr...@googlegroups.com.
To post to this group, send email to django-developers@googlegroups.com.
Visit this group at http://groups.google.com/group/django-developers?hl=en.
For more options, visit https://groups.google.com/groups/opt_out.


Reply via email to