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 post to this group, send email to django-d...@googlegroups.com.
To unsubscribe from this group, send email to django-develop...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.
On 15 September 2011 22:44, Jacob Kaplan-Moss <ja...@jacobian.org> wrote:
>
> #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.
As the ticket creator I feel obligated to reply :)
Thinking about it now, it does look kinda ugly. It's mainly because I
was focus on how to reuse existing decorators in CBV context. It
shouldn't be to hard to make the "view_decorator" a meta-decorator
(or a factory function as you call it) that turns login_required to
something that is both a class and function decorator. Methods are
still out-of-bounds for non-runtime introspection, but after almost 1
year of using CBV, I never needed to decorate a method.
I would like to also comment on the new approach in that ticket.
Making a shallow copy of a class is *MAGIC* to me. It breaks the basic
invariant "issubsclass(decorator(A), A) == True". This is important if
you're planing to use this as "B = decorator()(A)" (and you are,
'cause the whole point of not modifying the original class is to allow
safely doing this), as you quickly end up with weird alternate
hierarchies. So, please don't do that :)
>
> 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*.
+1 on this. I would be a bit concerned about this vs. future
implementation of generator-based-views that allow some kind of
response streaming (is someone working on that?).
>
> I've written a proof-of-concept patch to @login_required (well,
> @user_passes_test, actually):
>
> https://gist.github.com/1220375
>
Did I already mention creating a subclass in the class decorator
breaks super ;) [https://gist.github.com/643536]
> Can I get some thoughts on this technique and some feedback on whether
> it's OK to apply to every decorator built into Django?
>
It would be great to have that meta-decorator, so everyone else could
upgrade their decorators just by decorating them.
--
Łukasz Rekucki
Omit this part, It's late and I read HttpResponse by mistake ;), which
of course doesn't make any sense.
--
Łukasz Rekucki
--
Łukasz Rekucki
That sounds a lot better to me.
On Sep 16, 2011, at 12:11 AM, Łukasz Rekucki wrote:
> Hi,
>
> On 15 September 2011 22:44, Jacob Kaplan-Moss <ja...@jacobian.org>
> wrote:
>>
>> #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.
>
> As the ticket creator I feel obligated to reply :)
Me (as the poster of the latest patch) too :)
> Thinking about it now, it does look kinda ugly. It's mainly because I
> was focus on how to reuse existing decorators in CBV context. It
> shouldn't be to hard to make the "view_decorator" a meta-decorator
> (or a factory function as you call it) that turns login_required to
> something that is both a class and function decorator. Methods are
> still out-of-bounds for non-runtime introspection, but after almost 1
> year of using CBV, I never needed to decorate a method.
>
> I would like to also comment on the new approach in that ticket.
> Making a shallow copy of a class is *MAGIC* to me. It breaks the basic
> invariant "issubsclass(decorator(A), A) == True". This is important if
> you're planing to use this as "B = decorator()(A)" (and you are,
> 'cause the whole point of not modifying the original class is to allow
> safely doing this), as you quickly end up with weird alternate
> hierarchies. So, please don't do that :)
I agree that in an ideal world we'd have a better solution. On the
other hand, the cbv-decorator using inheritance just didn't work, and
the one using shallow copies does (if you don't use B=decorator()(A),
indeed).
As Donald mentioned, the more elegant solution for classes is to use
base classes (mixins) instead of class decorators. I'm not sure though
if every decorator can be replaced by a mixin, and if the order of
these mixins can be arbitrary in that case (which I believe is
desirable). Can anybody comment on that?
>> 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*.
>
> +1 on this. I would be a bit concerned about this vs. future
> implementation of generator-based-views that allow some kind of
> response streaming (is someone working on that?).
>
>> I've written a proof-of-concept patch to @login_required (well,
>> @user_passes_test, actually):
>>
>> https://gist.github.com/1220375
>
> Did I already mention creating a subclass in the class decorator
> breaks super ;) [https://gist.github.com/643536]
>
>> Can I get some thoughts on this technique and some feedback on
>> whether
>> it's OK to apply to every decorator built into Django?
>
> It would be great to have that meta-decorator, so everyone else could
> upgrade their decorators just by decorating them.
If this doesn't exist, then the view_decorator still has a right to
live.
So what would have to be created is a meta-decorator
universal_decorator, replacing or using view_decorator, such that
universal_decorator(login_required) is the actual universal decorator.
This would be applied to all django-decorators, but I think it would
be good to also allow applying universal_decorator to an already
universal decorator, such that:
# after this...
login_required = universal_decorator(login_required)
# ... this will hold
assert login_required == universal_decorator(login_required)
> class ProtectedView(MyView):
> login_required = True
>
> Where 'MyView' checks all the view's options during a dispatch. Never
> liked inheritance with multiple base classes...
How would I create my own 'decorators' in this approach?
Cheers, Roald
from django.contrib.auth.decorators import login_required,
permission_required
class ProtectedView(MyView):
view_decorators = [login_required,
permission_required('foo.can_do_bar')]
The difference to actually decorating the class is small, but I think
the above is a bit more explicit about what is happening.
Mixins sound like a good alternative, but does this just move the
problem in to mixin class instead of the base view class? That is, how
should the mixin class be implemented taking in account that you should
be able to decorate the function with multiple decorators? It seems you
would need something like ViewDecoratorMixinBase to take care of all the
dirty details...
- Anssi
There was a similar proposal on this list before introducing CBV. It
didn't gain much love.
--
Łukasz Rekucki
Surprisingly, I agree. Using decorators to *add functionality* is a
bad idea. But `login_required` or `csrf_exempt` are not strictly
functionalities. They're preconditions. They act exactly like any
piece of middleware, but instead of applying them globally to all
view, you can do it per-view. (My view_decorator intentionally uses
as_view() instead of dispatch(), so that the decorators are called
*before*, any view login is called). If you think about it this way,
applying decorators to CBV makes a lot of sense.
--
Łukasz Rekucki
Nice to meet you.
>
>> Thinking about it now, it does look kinda ugly. It's mainly because I
>> was focus on how to reuse existing decorators in CBV context. It
>> shouldn't be to hard to make the "view_decorator" a meta-decorator
>> (or a factory function as you call it) that turns login_required to
>> something that is both a class and function decorator. Methods are
>> still out-of-bounds for non-runtime introspection, but after almost 1
>> year of using CBV, I never needed to decorate a method.
>>
>> I would like to also comment on the new approach in that ticket.
>> Making a shallow copy of a class is *MAGIC* to me. It breaks the basic
>> invariant "issubsclass(decorator(A), A) == True". This is important if
>> you're planing to use this as "B = decorator()(A)" (and you are,
>> 'cause the whole point of not modifying the original class is to allow
>> safely doing this), as you quickly end up with weird alternate
>> hierarchies. So, please don't do that :)
>
> I agree that in an ideal world we'd have a better solution. On the other
> hand, the cbv-decorator using inheritance just didn't work, and the one
> using shallow copies does (if you don't use B=decorator()(A), indeed).
The one that just alters the given class also works, has no magic and
has the same limitation - dont' do "B = decorator(A)". Honestly, I
never needed that, but I understand some people might. Also note, that
the "super() problem" is fixed in Python 3, where super() determines
the base class from the call frame instead of referencing a global
variable.
>
> If this doesn't exist, then the view_decorator still has a right to live.
>
> So what would have to be created is a meta-decorator universal_decorator,
> replacing or using view_decorator, such that
> universal_decorator(login_required) is the actual universal decorator. This
> would be applied to all django-decorators, but I think it would be good to
> also allow applying universal_decorator to an already universal decorator,
> such that:
>
> # after this...
> login_required = universal_decorator(login_required)
> # ... this will hold
> assert login_required == universal_decorator(login_required)
>
>
+1
--
Łukasz Rekucki
Hi Łukasz,
On 09/16/2011 03:42 AM, Łukasz Rekucki wrote:
> The one that just alters the given class also works, has no magic and
> has the same limitation - dont' do "B = decorator(A)". Honestly, I
> never needed that, but I understand some people might. Also note, that
> the "super() problem" is fixed in Python 3, where super() determines
> the base class from the call frame instead of referencing a global
> variable.
The failure mode for "just alter the given class" (i.e. monkeypatching)
is much worse if you're applying the decorator to a class defined
elsewhere (e.g. a non-subclassed generic view; and applying, say,
login_required to a generic view is a relatively common case). It
modifies the generic view itself, and suddenly every use of that view in
your entire codebase is login_required. Nasty and surprising bug, that.
Compared to this (and breaking super()), "weird alternate hierarchies"
and breaking issubclass is a minor issue. Not to say that I like it -
but we're dealing with a set of options that are all pretty bad, IMO.
Carl
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iEYEARECAAYFAk5zNRgACgkQ8W4rlRKtE2didwCghtDhwFjGgnMKySa6egPRAnnI
ka4AnigoQlKV+//MUH6ze2LWzkra1CG3
=Duo1
-----END PGP SIGNATURE-----
> On 16 September 2011 10:17, Roald de Vries <down...@gmail.com> wrote:
>> On Sep 16, 2011, at 12:11 AM, Łukasz Rekucki wrote:
>>> I would like to also comment on the new approach in that ticket.
>>> Making a shallow copy of a class is *MAGIC* to me. It breaks the
>>> basic
>>> invariant "issubsclass(decorator(A), A) == True". This is
>>> important if
>>> you're planing to use this as "B = decorator()(A)" (and you are,
>>> 'cause the whole point of not modifying the original class is to
>>> allow
>>> safely doing this), as you quickly end up with weird alternate
>>> hierarchies. So, please don't do that :)
>>
>> I agree that in an ideal world we'd have a better solution. On the
>> other
>> hand, the cbv-decorator using inheritance just didn't work, and the
>> one
>> using shallow copies does (if you don't use B=decorator()(A),
>> indeed).
>
> The one that just alters the given class also works, has no magic and
> has the same limitation - dont' do "B = decorator(A)".
To show that this is actually a Python-thing, an analogous non-
decorator-example:
>>> class A(Base):
... def method(self, *args, **kwargs):
... super(A, self).method(*args, **kwargs)
...
... B = A
... A = SomeOtherClass
...
... B.method()
Traceback (most recent call last):
...
TypeError: unbound method method() must be called with A instance
as first argument (got nothing instead)
So IMHO it's "pythonic" to leave responsibility to use the decorator
in the right way to the programmer.
Cheers, Roald
My comments:
The class-decorator support isn't really universal, it's specific to
Django's built-in class-based views. This would need to be
well-documented, and the decorator should also probably check for the
existence of a "dispatch" method and say "sorry, don't know how to
decorate this class" if it's not there. (Could also check
issubclass(View) but that would prevent use with API-compatible CBVs not
descended from Django's base View).
I don't much like seeing an isinstance(HttpRequest) check introduced
(for the first time) as an implicit constraint on what can be passed to
a view. I think this is likely to break view tests in the wild using
non-HttpRequest-descended dummy requests (just like the test that fails
in Django). I don't think there's any good reason to call such tests
"broken" and make people change them, besides the desire to implement
this patch. More generally, the isinstance() requirement doesn't feel
very Pythonic.
Personally I think that the documented status quo for decorating CBVs is
adequate, if not ideal. The rundown here didn't mention the
"login_required(MyView.as_view())" approach that works in many, if not
most, cases and is simple, non-magical, and non-ugly (it also isn't
restricted to urls.py, you can just as easily do it in views.py right
under the class definition). I would be in favor of updating the docs to
recommend this approach even more strongly, showing more advanced usage
with decorator-factories (e.g. user_passes_test), and recommending the
method-decorator alternative only in the case where you want to decorate
a CBV and then subclass it (the wording in the current docs about
decorating "all instances" assumes that the as_view() approach can only
be used in urls.py).
Regardless of whether its a magical all-in-one decorator-decorator like
that proposed here, or something like class_view_decorator, IMO all of
the proposed approaches for class decoration on #14512 have serious
enough downsides that we're better off with the status quo.
Carl
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
iEYEARECAAYFAk5zTk0ACgkQ8W4rlRKtE2dhtQCg4GbN/Vht7KTZFiqMQsqvmt1/
VB8AoI69o+NHNnceR5q9p8kzC9BaNJ87
=2rSD
-----END PGP SIGNATURE-----
Gonna add this in here as well as ticket #14512I think using decorators to modify the way a CBV behaves is the wrong way to go about it, my reasoning is:1) Decorators on functions make sense, since the only way to modify a function is to wrap it, so decorators provide a shortcut to do so.2) There's already a standard way to add functionality to a class that has existed for a long time, is well tested, and as a bonus is something that people new to python but not new to programming will understand immediately.
Watch out, in general, with adding more and more mixins.
I explained just the most basic template CBV to a colleague: there are
just two mixins and a base class there, but his eyes already started to
glaze over because I had to jump from class to class to class to explain
how it all hung together.
Throw in a LoginRequiredMixin and SomeOtherMixin and you have the risk
that it turns into a big pile of un-debuggable code. Too many places to
look. Your mind's main working memory has a stack size between 6 and 8:
you just cannot juggle 3 base classes, 4 mixins and 2 method names at
the same time.
=> A mixin isn't necessarily clearer than a decorator.
Reinout
--
Reinout van Rees http://reinout.vanrees.org/
rei...@vanrees.org http://www.nelen-schuurmans.nl/
"If you're not sure what to do, make something. -- Paul Graham"
--
You received this message because you are subscribed to the Google Groups "Django developers" group.
To post to this group, send email to django-developers@googlegroups.com.
To unsubscribe from this group, send email to django-developers+unsubscribe@googlegroups.com.
that is my own reaction too when trying to see where in the code is
everything that i knew from the old generic view functions.
mixins are a great way to encapsulate specific, well defined behavior;
but tracing the code is a real chore, even so clean code as Django's.
I guess that when you know by heart exactly what does each one do,
they become a pleasure to use.
So, my two cents: please do the documentation first. once there's a
single place to find exactly what the purpose, mechanism and span of
each one, then adding more would be welcomed.
--
Javier
> Documentation is being worked on, and is orthogonal to the current
> discussion of how
> to handle things like requiring logins with the new CBVs.
I just watched "Class Decorators: Radically Simple" by Jack Diederich,
who wrote the class decorators PEP, and I think it's very useful to
watch (25 min.) for this discussion. According to him it is good
practice to return the class that is decorated, which I think we
should follow, and which solves the biggest practical problems with
decorating. Moreover, the fact that class decorators exist indicate
that they are pythonic. So +1 for the decorators.
Considering the mixins: IMHO, the order of base classes shouldn't
matter. Can this be satisfied by the mixin-approach?
@Carl Meyer: I would opt for applying decorators *in* the class, so
you can still derive from it. Like::
class MyView(View):
@classonlymethod
def as_view(cls, *args, **kwargs):
return login_required(super(MyView, cls).as_view(*args,
**kwargs))
Cheers, Roald
1. Django supports class-based views.
2. Django supports function-based views (ultimately these are the same
thing, which is that Django supports anything as a 'view' so long as
it's callable, accepts an HttpRequest as its first positional argument
when being called and either returns an HttpResponse or raises an
exception).
3. The natural way to add processing in/around a class is subclassing
and either overriding or mixing in.
4. The natural way to add processing in/around around a function is decorating.
Any solution to this needs to address those constraints, and allow
code to look and feel natural.
Based on that, some arrangement which allows the same basic logic to
exist in a "mixin" (I dislike that word) for classes and a decorator
for functions seems most appropriate, even if it does mean having two
ways to do things -- that's a distinction I think we can live with, as
people will appreciate that it doesn't result in strange-looking code.
--
"Bureaucrat Conrad, you are technically correct -- the best kind of correct."
--
You received this message because you are subscribed to the Google Groups "Django developers" group.
To post to this group, send email to django-d...@googlegroups.com.
To unsubscribe from this group, send email to django-develop...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.
unittest.skip isn't a Mixin, it turns the class into an exception and raises it.
django.test.utils.override_settings is a mixin and it's terrible, it dynamically creates a new subclass, and overrides 2 methods. It's magic and more complex then need be.
Your case seems to rest on what is "natural" - both in terms of how to
modify existing control structures and as a general goal that our
resulting syntax must "feel natural."
I submit that the way that will "feel natural" is to simply allow
decoration of any view, be it a class or a function, with the
decorator whose name describes precisely the functional modification.
This is simple to read, easy to remember, and, frankly, DRY.
--
Justin Holmes
Head Instructor, SlashRoot Collective
SlashRoot: Coffee House and Tech Dojo
60 Main Street
New Paltz, NY 12561
845.633.8330
They *are* a pleasure to use. A colleague switched an app over to class
based views last week and he got rid of 30% of the code. And it was more
elegant and readable.
Just mentioning that as I'm real happy with class based views :-)
I agree with all your points, except #3, I think it's an over simplification. There's another way to change a class: class decorators. And there's ample precedent for their use in such a manner, unittest's skip decorators, our own decorators for swapping settings during testing, etc.Alex
Alex