Skip to content

Fixed #29750 -- Added View.setup() hook.#10427

Merged
timgraham merged 1 commit intodjango:masterfrom
francoisfreitag:29750_pre_dispatch
Dec 22, 2018
Merged

Fixed #29750 -- Added View.setup() hook.#10427
timgraham merged 1 commit intodjango:masterfrom
francoisfreitag:29750_pre_dispatch

Conversation

@francoisfreitag
Copy link
Copy Markdown
Contributor

@francoisfreitag francoisfreitag commented Sep 22, 2018

Allows encapsulating logic in View mixins for reuse in child classes.

The standard request processing cycle is:

  1. Concrete view
  2. Mixins
  3. Generic view

Before init(), it was not possible to reuse self attributes set in parent views and mixins from the concrete view, because calling the parent method generates a HttpResponse.

https://code.djangoproject.com/ticket/29750

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @francoisfreitag. Thanks for this.

Looks OK. (Still not sure it's quite to my taste but whatever... 🙂)

Yes, I think a doc example is worthwhile. Yes, in the topic page.

The example you give in the description is nice. Except I was surprised to see you still override dispatch() in ImpersonationWithMessageMixin. Obviously that works but I thought the idea here was to allow us to leave dispatch alone? Would it not work equally implementing init() and calling super()? (With correct MRO mixins could rely on attributes being set this way — it'd potentially get tricky but it's up to each to keep their code clean and it's already an "Advanced Topic".)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a note here saying that you must call super() if overriding? (Or similar... obviously you could do it by hand...)

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

Hi Carlton,

Thank you very much for reviewing the PR and your input.

I was surprised to see you still override dispatch() in ImpersonationWithMessageMixin.

In my mind, init() is responsible for performing view attribute initialization and nothing else. The idea behind using dispatch() in the example is to avoid encouraging developers to perform other tasks in init().

[...] the idea here was to allow us to leave dispatch alone?

Not exactly, the idea is to provide parent views and mixins with a way to abstract logic from child classes, but let children control the response that's generated.
In the example, the child ImpersonationWithMessageMixin must know the user being impersonated, that is set by parent ImpersonationMixin. To execute the parent logic that sets self.user without init(), the child needs to call super().dispatch(), which generates a HttpResponse.


Maybe adding a message is not the best example, the example could be updated to check permissions. For example, we could return a HttpResponseForbidden when not self.user.is_active. That way, an admin can't record interest for inactive users.
Checking access permissions could be a better use of dispatch, and it is simpler (doesn't involve the message framework). I'll update the example before to add it to the documentation.


In the ticket, I had hopes to move all self.object = get_object() to init(), to avoid duplication in view methods, such as BaseUpdateView's get() and post(). However, this is backward incompatible: init() would trigger get_object () earlier in the view lifecycle and may issue DB queries when none were necessary before, because dispatch() may raise a PermissionDenied, then the get() or post() method wouldn't execute get_object().

@carltongibson
Copy link
Copy Markdown
Member

I had hopes to move all self.object = get_object() to init(), to avoid duplication in view methods, such as BaseUpdateView's get() and post().

I'd be super -1 on this. The get_object() call is an integral part of the view logic. Moving to init() just obscures the flow. A bit of repetition actually makes for clearer code here. (And in many places TBH. It's quite easy to take DRY too far. IMO)

@francoisfreitag francoisfreitag force-pushed the 29750_pre_dispatch branch 3 times, most recently from 5a7c1be to 57533c0 Compare October 19, 2018 21:40
Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I'm still not sure I like the feature. It's fine as implemented though.
  • I'm a bit worried about the docs example. I think it will encourage people down bad roads, rather than being a hook for that time when you really do need it.

What do others think? (I know you like it @jdufresne 😀)

@jdufresne
Copy link
Copy Markdown
Member

What do others think? (I know you like it @jdufresne)

Yeah, I do like it and I think François has done a great job here. I'm +1 on merging this once the docs meet your approval.

@charettes
Copy link
Copy Markdown
Member

What do others think?

My only concern here is the init() method name which doesn't make it clear it isn't directly related to __init__() IMO. Were alternative names such as setup() considered?

@jdufresne
Copy link
Copy Markdown
Member

I'm not too picky on the name. If setup() is more intuitive and agreeable, no problem here.

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Nov 7, 2018

My concern isn’t really the feature at this point. I wouldn’t go this way, but it’s fine and I can see the utility if it’s the style you want to use. So 👍 there.

I am concerned about the docs example: I think a lot of users take the docs as being normative, as saying do this!; I think unless you really know what you’re doing putting parts of your view logic in dispatch is not something you should do. (You should just keep it in your view function, even if that means a bit of repetition calling stepOne(), stepTwo() and so on.) Thus I think the example is tacitly providing the wrong advice for most people. (A small docs entry highlighting the feature for someone who’s already looking for it would be better I think.)

If no one else is feeling this then I’m happy to let it pass. (It’s quite possible I’m just too old. 🙂) But that’s why I asked for other opinions (because I can’t shake the feeling, having given it time enough for a change of heart.) Happy to merge it, happy to release it, but I’d basically like to recuse myself on making the decision here. Thanks for your help all.

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

Many thanks @carltongibson for approaching this PR with such an open mind!

I removed the example from topics and added a few lines in the View's init() section. Please let me know if that's closer to what you had in mind.

If there's still doubt around this feature, I am happy to open a discussion on the mailing list. The previous discussion did not reach a consensus and other community members may have different perspective on this hook. I could post the removed example on the mailing list to describe the use case and gather opinions. What do you think?

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

As for the name, I kept init() because it's short and the hook's purpose is to initializes view attributes. I am happy to change it to setup if it helps understand what this hook can be used for. It's clear that setup() will run before dispatch() and I would not expect setup() to return anything.

init() was the name suggested in the previous mailing list discussion. For a while, I had pre_dispatch() in mind, but that's not entirely what this hook does. It allows parent code to execute before child code, so I dropped that idea. That's the names that made sense to me, I'm definitely open to alternatives.

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Nov 10, 2018

Hi @francoisfreitag Thanks for the follow-up.

I don't know that we need to go to the mailing list. Jon like feature, Simon has expressed support, I see the use of it (even if it's not my thing per se) — let's go with it.

Only thing now is the docs. (And maybe a rename.) I like the changes you've made: they're much more in line with how I'd present it.

Allow me to give it another once-over and we'll see about getting it in.

Thanks for effort!

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @francoisfreitag.

I pushed some edits in 4cb1243.

Can you have a look? Thanks!

Copy link
Copy Markdown
Contributor Author

@francoisfreitag francoisfreitag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edits look good to me, thank you.

In the end, we won't provide use cases to explain the feature. Maybe we should add the use case for user impersonation suggested in a previous version of this PR to the ticket? That may help future readers understand the use case. What do you think?

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Nov 14, 2018

My thought was that it's fine as-is, i.e. minus the example. (I think people looking for it can come up with their own use-cases.) There's plenty of discussion on the ticket (I think).

I also thought that your use-case would make a good blog-post, on your own or company blog. (No harming in throwing a link to such on the ticket...)

@carltongibson
Copy link
Copy Markdown
Member

Were alternative names such as setup() considered?

So I was just about to mark this RFC when I remembered this. Naming things...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if not hasattr(...) and raising an exception would be more in line with Django's style.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can omit the tuple and just put this on the previous line.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chop comma

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a, b, and c (missing comma after 'b')

also missing .. versionadded:: 2.2 annotation

Also you mentioned a use case in the release notes but not here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "The new" is more inline with other notes.

View.init <django.views.generic.base.View.init> would render "View" also which would help.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would replace "class..." with "view" (no link)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It allows....

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use self.assertRaisesMessage.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this in the one test they're used in unless you think they have potential to be used in future tests.

@carltongibson
Copy link
Copy Markdown
Member

Hey Tim. Thanks for the review. Do you have any thoughts on the naming question?

@timgraham
Copy link
Copy Markdown
Member

No strong opinion. One idea is that it the name init could create some ambiguity with __init__ when speaking.

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Nov 19, 2018

No strong opinion...

OK, let's call that a +0.5 🙂 I'll say the same for Simon and myself (for total +1.5)

@francoisfreitag Could you change for Simon's suggestion of setup(). I don't suppose there's much in it but it avoids the verbal confusion as Tim says?

Then if you could address Tim's review comments and rebase I think we're there. 🎉

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

I don't mind changing to setup(). Thanks for the feedback, I'll update the PR in a few days.

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

Early commit to address comment "Use self.assertRaisesMessage." introduced as #10714.

Ready for review.

@timgraham timgraham changed the title Fixed #29750 -- Added init() hook to View Fixed #29750 -- Added View.setup() hook. Dec 6, 2018
@francoisfreitag francoisfreitag force-pushed the 29750_pre_dispatch branch 2 times, most recently from c1022f4 to 0af6de3 Compare December 12, 2018 10:10
@francoisfreitag
Copy link
Copy Markdown
Contributor Author

Sorry @timgraham, I push-forced your edits. I restored them from https://github.com/django/django/compare/53d05c3d166eb2f5e410b7a6c2c947a6480b1c50..c1022f47e051924288d2d5cfbdf7c7cfecd59a79, but would not mind a second set of eyes.

I'll make sure to use --force-with-lease in the future.

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

The commit message was reverted. I was only able to restore the first line.

@francoisfreitag
Copy link
Copy Markdown
Contributor Author

Note: I did make a change on my side (CheckInitView -> CheckSetupView).

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. This looks ready to go. Thanks for the effort @francoisfreitag! 🎁

Just the one observation...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preferred the shorter version we had earlier:

Initializes view instance attributes: ``self.request``, ``self.args``
and ``self.kwargs`` prior to :meth:`dispatch`. Child classes overriding
this method must call ``super()``.

Copy link
Copy Markdown
Contributor Author

@francoisfreitag francoisfreitag Dec 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restored it from https://github.com/django/django/compare/53d05c3d166eb2f5e410b7a6c2c947a6480b1c50..c1022f47e051924288d2d5cfbdf7c7cfecd59a79#diff-2df22e647d63040c7db3eca921c888fdL87, I believe this change was part of @timgraham's cosmetic edits.

The longer version explains the purpose of this hook, that's why I have a small preference for it.

@timgraham timgraham merged commit e671337 into django:master Dec 22, 2018
@francoisfreitag francoisfreitag deleted the 29750_pre_dispatch branch December 26, 2018 10:03
felixxm pushed a commit to smithdc1/django that referenced this pull request Apr 10, 2020
felixxm pushed a commit that referenced this pull request Apr 10, 2020
def test_not_calling_parent_setup_error(self):
class TestView(View):
def setup(self, request, *args, **kwargs):
pass # Not calling supre().setup()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/supre/super/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(was fixed in b916c27)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants