Reduce dependence on ViewerModel, using Layer(List) instead#125
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #125 +/- ##
==========================================
+ Coverage 94.47% 97.06% +2.58%
==========================================
Files 11 11
Lines 1429 1429
==========================================
+ Hits 1350 1387 +37
+ Misses 79 42 -37 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
# References and relevant issues Meeting 12 hours ago with @carlosmariorr we built this with an LLM together. We noticed some bad patterns which is now where #125 was born from. # Description This adds callbacks to layer events so that there is now bidirectional communication between layers and widget values. I'm not sure how important it is to review this, because #125 bludgeons much of the logic. @carlosmariorr what do you think? We discussed this and observed its working performance this morning, and the event connections continue in #125.
| if self._selected_layer is None: | ||
| return |
There was a problem hiding this comment.
I guess I need to leave this comment up.
Is this necessary?
Just context for the archive:
Layer cannot be None anymore in the AxisComponentBase but the inheriting classes (e.g. AxisLabels in _axis.py) have this check in their _on_value_changed() method which I believe is not necessary if the Layer should not be set to None.
I think that the way to handle this is to make it so that in the even of layer changed to None, the widgets are destroyed.
I'll take a look at #127 to check what this is doing and then come back to this.
There was a problem hiding this comment.
No more of these should exist now.
…ng LayerName edit
| scales = layer.scale | ||
| for value in scales: | ||
| sb = QDoubleSpinBox(parent=self._parent_widget) | ||
| sb.setDecimals(3) | ||
| sb.setSingleStep(0.1) | ||
| sb.setRange(0.001, 1_000_000) | ||
| sb.setRange(self._SCALE_MINIMUM, 1_000_000) |
There was a problem hiding this comment.
we remove the old method which clamped the axis values, and now just use the widget behavior.
This still works fine with tests. We can increase to 4 decimals if needed
| @@ -106,11 +95,70 @@ def component_label(self) -> QLabel: | |||
| return self._component_qlabel | |||
|
|
|||
| @abstractmethod | |||
| def load_entries(self, layer: Layer | None = None) -> None: | |||
| """Load or refresh widget state for *layer* (defaults to active).""" | |||
| def load_entries(self, layer: Layer) -> None: | |||
| """Load or refresh widget state for *layer*.""" | |||
There was a problem hiding this comment.
Two noticeable changes here that effect everything since this is our base ABC
- we don't take viewer anymore
- there is no optional layer for load_entries
|
|
||
| def __init__(self, *args, **kwargs) -> None: | ||
| super().__init__(*args, **kwargs) | ||
| self._selected_layer: Layer | None = None |
There was a problem hiding this comment.
this is where we init layers, if needed, with None, before they get loaded
| def _require_selected_layer(self) -> Layer: | ||
| layer = self._selected_layer | ||
| if layer is None: | ||
| raise RuntimeError( | ||
| f'{type(self).__name__} is not bound to a layer.' | ||
| ) | ||
| return layer |
There was a problem hiding this comment.
this is our technical Qt safety for widget references during teardown, so we can call layer = self._required_selected_layer() to ensure that a Layer is returned, and no longer None, this centralizes all the condition if self._selected_layer is None: checks
| return layer | ||
|
|
||
|
|
||
| class BoundLayerCoordinator(BoundLayerOwner, ABC): |
There was a problem hiding this comment.
This ABC is focused on safely referencing layers as they are selected, because I noticed shared logic in so many widgets
| """Return the display string for *layer*.""" | ||
|
|
||
|
|
||
| class BoundFileComponentBase(BoundLayerOwner, FileComponentBase): |
There was a problem hiding this comment.
this class was created for the LayerName widget, which requires safely naming layers and is more than just a callback to a layer property
| if self._selected_layer is not None: | ||
| for component in self._components: | ||
| component.load_entries(self._selected_layer) | ||
| layer = self._require_selected_layer() |
There was a problem hiding this comment.
here is where our None checks live like now
| # Wire layer list events directly | ||
| self._layers.events.inserted.connect(self._update_layers_combobox) | ||
| self._layers.events.removed.connect(self._update_layers_combobox) | ||
| self._layers.events.changed.connect(self._update_layers_combobox) |
There was a problem hiding this comment.
layerlist event wiring, rather than using the separate things in layer_utils
| self._general_metadata_instance.unbind_layer() | ||
| self._axis_metadata_instance.unbind_layer() |
There was a problem hiding this comment.
again, we see here how things like unbinding takes care of both disconnecting events and other parts of the widget deconstruction/reconstruction
There was a problem hiding this comment.
this file removes all layer objects that already exist, and only is used to query things, like datatype, that are not a guaranteed property. Now we just use layer callbacks everywhere
|
MErging after long talk with @jni in community meeting. Will work on codesmells later, and hopefully left enough breadcrumbs here |
References and relevant issues
Built on #124 diff: TimMonko/napari-metadata@enh/layer-events...TimMonko:napari-metadata:enh/reduce-complexity
Born out of discussion about 12 hours ago with @carlosmariorr
Description
Significantly cleans up the code by only placing the ViewerModel inheritance at the npe2 MetadataWidget (_main.py) boundary. Other widgets now only need to inherit Layer (Axis and File widgets) or LayerList (Inheritance Widget), this creates significantly easier reasoning about what should happen in each widget.
Because we know that the subwidgets are only shown in the container if a selection is active, we also can assume there will never be a None layer. So, now we also don't need
resolve_layerand instead can just depend directly on public layer API. Effectively nowlayer_utils.pyonly contains special logic (a good example of this is the dtype one). Because of widget construction lifetimes, we still do need to check for Nones (unfortuantely), so there is some complexity here still, but at least we don't have if None checks everyone.This is going to be slightly annoying to review, but one key is that tests are either removed (layer_utils functions being gone) or are simpler because they drop ViewerModel and are typed differently. As such, the test suite passing with no logic changes indicates that this doesn't break functionality.
I also tested in napari itself locally, and all events are still hooked up well.