-
-
Notifications
You must be signed in to change notification settings - Fork 409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Event handler refactor for image layer #1376
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1376 +/- ##
==========================================
- Coverage 87.80% 87.79% -0.01%
==========================================
Files 238 239 +1
Lines 22020 22048 +28
==========================================
+ Hits 19334 19358 +24
- Misses 2686 2690 +4
Continue to review full report at Codecov.
|
Interestingly this now has the failing windows test, which may previously have been fixed by #1347 and our mysterious scipy stats import. Not sure why this has reappeared here as that code should have been merged in ... The mystery there continues @tlambert03, but I wonder if this PR gives any clues .... |
Windows tests were fixed by #1377, all tests are now passing!! |
I don't really. I think it would be nicer to write transform functions that get imported and used as needed.
I prefer things being based on the received value. I think this will make for more modular code and will make things like shrinking the number of object attributes much easier.
Partial views are a very interesting use case, you're right. I still think it would be nice to document our models somehow with my dataclass "spec", so that people know what a complete view looks like. I think there is a middle ground we need to find here. For example, one use case I was envisioning was a view that ignores interpolation changes, because they only support nearest neighbour interpolation. (For example, the thumbnails could be views of the layer!) However, it's good to know that the view is doing this ignoring, and have the ignoring be explicit somehow, rather than have it be implicit. There also needs to be a check that all public attributes have a corresponding event. How do you like this test idea: construct a "view" of the Image layer that is in fact another image layer. Randomly mutate the values of the original layer, serialize them both, and check that the serializations are identical.
Can you elaborate on this? I wasn't aware that we were diverging from VisPy, and I'd like any changes here to be somehow mirrored in a VisPy PR. Maybe that looks like separating VisPy events into BasicEvent and FancyEvent (aka Event), and we only use Basic. But I'd prefer that we don't diverge.
Why would it be bad to have multiple handlers? Isn't the idea to have one emitter (per model instance), many listeners?
I actually was hoping to keep that discussion separate and happening after this PR. They interact in lots of ways and trying to consider both changes simultaneously feels like too much to chew on. I think we can probably handle this first and then simplify the models — and having well defined interfaces as discussed in point 2 will only help with that. For example, in my dataclass model, that's not actually the full interface but rather |
Had good chats with @shanaxel42 and @jni about this PR today, and there is good alignment. A couple key takeaways (numbering aligns with numbers above).
I will try and add a |
So after more investigation the case of forcing the |
Yeah the order of events that makes sense to me is:
For step 2, you need to know which layer changed its name. This can only work by knowing the event source or by emitting an (old_name, new_name) tuple rather than just the new name. The only other alternative I see is that layers don't in themselves have names. Instead, only the layer list gives them names. Then we are sidestepping this whole dance and this will always be handled by the layer list. |
maybe this is already obvious (can't tell by the phrasing here), but just wanted to point out that the event object does have a |
To make that possible @jni we need to add a concept to the event handler of "priority" or "ordering" of the callbacks, as we need to make the layerlist a listener to the layer but make sure that it's callback executes last (otherwise even if it renames the layer during it's update there will be other updates that have yet to happen and when they do they will use the old name and things will get out of sync). Also if the layerlist update happens last at that point we'll have two layers with the same name and know knowledge of which was which, and which had the old name as the update has already happened. Passing a tuple of
Indeed, but we've lost that by the time the [EDIT: I'm really struggling to see how we can make this use case possible without some major changes] |
I'm going to try a new approach which will use two events instead of one .... |
I don’t think this matters. In step 3, the layer list updates the layer name. At that point, the layer eventhandler emits events with the new name, and of I’m not mistaken these will necessarily be processed by listeners after the original name change — so things will get in sync after just a few “clock cycles”. Am I missing something? |
This is really about guaranteeing that step 1 is complete before this change happens, i.e. if the layer list event handler can make this change while the other event is still processing then it won't result in serial updates, it will result in a nested update that then gets overwritten. Certainly the implementation I tried suffered from this in practice. If the layer widget emits name A then the layerlist hears it first (which may happen with no "priority"/ "ordering") and then changes it to A_1 then emits again then the layer hears it takes on A_1 and that event finishes, but then the original A from the layer widget come in and sets the layer name back to A!!! The "two event approach" might work, though |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sofroniewn very good! I've left mostly very minor comments, or requests for clarification. I would say the biggest concern is that I'm unsure again about unifying on value
, as it makes the actual code more difficult to read when it's not a one-liner.
I also think a high priority for this refactor is to enshrine @shanaxel42's information flow diagram to the docs somewhere so that new contributors are comfortable with the napari eventhandler model.
I'll approve because I don't want to hold this up any longer, but stuff to think about. Everything else is just typos. =) Thanks to all involved!!!
# Once EVH refactor is done, these can be moved to an initialization | ||
# outside of this object | ||
self._on_opacity_change(self.layer.opacity) | ||
self._on_blending_change(self.layer.blending) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this for me? Currently I found it confusing that this wasn't happening in this refactor. At any rate, I think at least the _on_*_change
calls should get bundled with their respective box initialisations... (ie move _on_opacity_change
to line 67 and on_blending_change
to line 72)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this is a good question. It's hard right now as we don't have an explicit concept of a controller which is I guess where these imports, initialization and wiring up would be done (as it would be the one place which knows about both model and view). We can't do it in the event handler itself as we import the event handler into the layer model directly so that we can edit the layer model without the views, so we need an additional concept here. I'll work on some architecture docs in a new PR and we can revisit this there.
As to the grouping, I was grouping the soon to be obsolete properties together to make easier to remove them later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooo - ok, I have one idea - we could add it to the register_listener
method of the event handler. It would be nice if we had a standardized way of figuring out what the event values should be and then re-emitting them. i.e. when you register the qt_image_layer
we see that it has the _on_visible_change
method and we know that the Image
layer emits the visible
event, and we know the current visible
value lives at layer.visible
so we call _on_visible_change(layer.visible)
for the qt_image_layer
?
We might be baking in a lot of assumptions there, but it would remove the need to a lot of initialization code throughout the repo. Curious @tlambert03 @jni @ziyangczi what you think of this idea?
# Once EVH refactor is done, these can be moved to an initialization | ||
# outside of this object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above... I'd like a bit of clarification of when this is supposed to happen (ie what exactly has to be done), and maybe move the items to their respective sliders, although now that it's more things to change than two I see the value of grouping near-obsolete things together...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See discussion above
@@ -451,7 +450,7 @@ def data(self, data: np.ndarray): | |||
elif len(data) > cur_npoints: | |||
# If there are now more points, add the size and colors of the | |||
# new ones | |||
with self.events.set_data.blocker(): | |||
with self.events.slice_data.blocker(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you comment on why we need blockers here? I thought the whole point of the refactor was to not need blockers? Or is it because the refactor isn't actually done with the points layer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Points
refactor not done yet - hopefully this will go when it is!!!!
@@ -243,7 +243,8 @@ def data(self, vectors: np.ndarray): | |||
self._displayed_stored = copy(self.dims.displayed) | |||
|
|||
self._update_dims() | |||
self.events.data() | |||
self._update_editable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's editable exactly? I noticed @tlambert03 asking about it in another comment but to be honest the answer didn't really clarify anything for me...?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Editable is both a user setting and an attribute automatically determined that sets where things like adding points/ drawing with the paintbrush etc are allowed or not. We added both because someone might want to disable such functionality themselves AND because there are certain configurations like multiscale or 3D viewing where we don't support such interactivity. Definitely room for improvement here, but that's what the parameter is
) | ||
args = self.default_args.copy() | ||
args.update(kwargs) | ||
return self.event_class(**args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oof. This whole change makes me dislike the value
kwarg even more. Perhaps the keyword can be the event name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually don't mind value
on the event as the one place where the payload is being carried. To me this makes a lot of sense and is fairly readable, but I see your comment about value
inside the handlers and think we should change it there. @tlambert03 thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I’m on mobile at the moment and can’t see the full conversation. I don’t care what you call the argument in the handler... but I agree with @sofroniewn that the event payload should be standardized. I hated having to go find the event emitter to figure out the name of the event object attribute I’m supposed to be querying to retrieve the value. I also don’t mind this code block either if it lets us make connections easier.
@jni is your main objection the argument name in the handler? Or do you want the actual event object to have variable attribute names
I don't know about this specific instance, but in general the idea we've had with vendoring is to allow us to incorporate a quick bug or behaviour fix but always with the idea of contributing upstream and eventually removing the vendored copy. Whereas modifying entails a full divergence and I hate that — I want to play nicely with the ecosystem and reduce redundancy. |
(which would argue against the modifications to the event classes in this PR...) |
@sofroniewn please rename this PR to something less self-referential before merging! =) |
Co-authored-by: Juan Nunez-Iglesias <juan.nunez-iglesias@monash.edu>
@jni thanks for the review! Did you have any feedback on the decorator approach instead of There's some discussion happening in the comments about where we will initialization of our views. We need to think about that more, I feel like we might need a controller concept, see here #1376 (comment) as it would be the one place that knows about both models and viewers, but it is nuanced. As to using I'll work on an architecture doc in a new docs only PR |
FWIW, I’m extremely Luke warm on the decorator as it stands. I would support merging this and revisiting if needed (it’s mostly additions to this rather than replacements) |
status=Event, | ||
) | ||
|
||
# When the EVH refactor #1376 is done we might not even need the layer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shall we use a TODO signature? haven't seen much in napari codebase, is it discouraged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open - thoughts @jni? @tlambert03?
visible=Event, | ||
) | ||
|
||
# When the EVH refactor #1376 is done we might not even need the layer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above for TODO
return | ||
|
||
# Update based on event value | ||
for component in self.components_to_update: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we talked about this regarding having annotation-based detection vs name-based detection, wonder what others' opinion on this too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See discussion above and bellow this comment, and links in this comment for potential implementation #1376 (comment)
if name not in names: | ||
return name | ||
else: | ||
return force_unique_name(inc_name_count(name), names) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if the forced name already exists? shall this method loop and check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The looping and checking is happening recursively as the original function is called force_unique_name
and then calling itself inside itself.
agreed with talley, let's merge this one and think more on the decorator. I still love to have a decorator based detection, but like talley said it is more complicated and introduced a lot of lines. (maybe a composite method where we name search and use a decorator for special cases?) |
This reverts commit e43f730.
* Revert "Use qlistwidget for QtLayerList (#1391)" This reverts commit 33e7a74. * Revert "Event handler surface layer (#1396)" This reverts commit 5acd648. * Revert "Fix vispy volume colormap changing (#1402)" This reverts commit 9ea724b. * Revert "Event handler refactor for image layer (#1376)" This reverts commit e43f730. * partial return of qlayerlist * Revert "partial return of qlayerlist" This reverts commit 897c454.
Description
This PR is from the same branch as is PRed here shanaxel42#2 to #1040 directly. Due to my badness at git though that PR has all changes from master in it making it hard to review so I'm just going to make this one directly. It will also let me check if all tests pass on our CI.
Copying over my last comment from there:
The PR should be >99% backwards compatible in that no tests changed, I only added tests. Some private methods/ attributes changed. I don't think any public ones did, certainly not public ones that are part of our main API.
I will write more about this elsewhere, but the PR is a bit of a hodgepodge in places as we havn't fully moved over to the event handler for all attributes, all layers etc, or the dims models, but at least all functionality is working and it will be more clear how to extend things in the future.
There are a couple major points to discuss
Do we like this
_transform_*_change
method that I added which allows us to preprocess data on events inside the event handler before propagating to all listeners. It was basically essential for the name case I described above, but I can see it being useful elsewhere, see comment in #1040.Do we want all our view like objects now only to make updates based on the value they receive in the
_on_*_change
method or can they continue to have access to the whole data model. As has been pointed out in a number of comments now this hybrid is a little odd, see our colormap handling and comment in #1040.Do we need the Interface classes, and if so how should they look? I went the the checking if attr exists approach instead as sometimes I wanted to register a listener that only implemented an update method for one or two of the methods, see how I added listeners for the layerlist which has the _transform_name_method and the viewer which only has a few like _on_help_change. If each one of these had to have its own interface or if we allowed more partial interfaces then I'm not sure what's gained by having them at all, see comment in #1040.
We should revisit the changes to the Event class we pulled from vispy and make our changes backwards compatible or clearly document why we needed to make breaking changes there (for example the autoconnect stuff, which although we were setting before I don't think we were really making much use of). See comment in #1040 and comment in #1040.
We should think about how many event handlers we're going to need and where they should live and how they might talk with each other. I had to register the viewer and the layerlist to the layer event handler to get some of the stuff to work (name and active_layer) is this bad, is this ok? Will viewer get one and dims get one? Then maybe we are good?
We should think a bit about where more controller like stuff will live (thinking now about things like Layer models #1353) and the slicing of data. At least working that example out a bit before merge will probably be good.
There are also a couple minor points
A. We should add tests specifically for all the event handler stuff.
B. We should clean up all the doc strings, many are out of date now or wrong or missing.
C. We should think if we like having value everywhere in function signatures or if we want the name of the property etc.
We can always close this PR and work with the other ones, there are some great discussions happening there, or we can just press on with this one. I don't mind.
Probably next steps are in @napari/core-devs and @0x00b1 can weigh in on some of the big picture questions I mentioned above, and maybe @kne42 can go through and clean up some of the doc strings/ standardize function signatures/ add more comprehensive testing dedicated to the new functionality.
Type of change
Final checklist: