Repository: napari/napari

Issue:
Cannot change axis labels just after layers are cleared.
## 🐛 Bug

When viewer dimension is changed from 3D to 2D, renaming axis labels causes `IndexError`.

## To Reproduce

Steps to reproduce the behavior:

1. Add 3D image.

```python
import napari
viewer = napari.Viewer()

viewer.add_image(np.random.random((100, 100, 100)))
```

2. Clear layer list and change axis labels **at the same time**

```python
viewer.layers.clear()
viewer.dims.axis_labels = ["y", "x"]
```

Then I got

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_17004/2902543546.py in <module>
      1 viewer.layers.clear()
----> 2 viewer.dims.axis_labels = ["y", "x"]

~\Anaconda3\envs\mt\lib\site-packages\napari\utils\events\evented_model.py in __setattr__(self, name, value)
    149         are_equal = self.__eq_operators__.get(name, operator.eq)
    150         if not are_equal(after, before):
--> 151             getattr(self.events, name)(value=after)  # emit event
    152
    153     # expose the private EmitterGroup publically

~\Anaconda3\envs\mt\lib\site-packages\napari\utils\events\event.py in __call__(self, *args, **kwargs)
    597                     continue
    598
--> 599                 self._invoke_callback(cb, event)
    600                 if event.blocked:
    601                     break

~\Anaconda3\envs\mt\lib\site-packages\napari\utils\events\event.py in _invoke_callback(self, cb, event)
    629                 self.disconnect(cb)
    630                 return
--> 631             _handle_exception(
    632                 self.ignore_callback_errors,
    633                 self.print_callback_errors,

~\Anaconda3\envs\mt\lib\site-packages\napari\utils\events\event.py in _invoke_callback(self, cb, event)
    618     def _invoke_callback(self, cb: Callback, event: Event):
    619         try:
--> 620             cb(event)
    621         except Exception as e:
    622             # dead Qt object with living python pointer. not importing Qt

~\Anaconda3\envs\mt\lib\site-packages\napari\_qt\widgets\qt_dims_slider.py in _pull_label(self, event)
    190     def _pull_label(self, event):
    191         """Updates the label LineEdit from the dims model."""
--> 192         label = self.dims.axis_labels[self.axis]
    193         self.axis_label.setText(label)
    194         self.axis_label_changed.emit(self.axis, label)

IndexError: tuple index out of range
```

## Expected behavior

Since viewer is 2D after layer list is cleared, axis labels should be renamed to "y" and "x" respectively.

## Environment
```
napari: 0.4.12
Platform: Windows-10-10.0.22000-SP0
Python: 3.9.7 (default, Sep 16 2021, 16:59:28) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.6
NumPy: 1.20.3
SciPy: 1.7.2
Dask: 2021.11.1
VisPy: 0.9.3

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.9621
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1368x912, scale 2.0

Plugins:
- console: 0.0.4
- scikit-image: 0.4.12
- svg: 0.1.5
```

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Looks like this is fixed in the latest release (v0.4.15) and maybe before, though I'm not sure what fixed it. We could run `git bisect` if we're curious enough, but maybe instead - @hanjinliu can you confirm this is fixed and close this issue if it is?
Seems it's not fixed on my side... got the same error.

Here's my current environment. I also tested with v0.4.15 and v0.4.16rc1.
```
napari: 0.4.16rc2.dev32+g171e1c74
Platform: Windows-10-10.0.22000-SP0
Python: 3.9.7 (default, Sep 16 2021, 16:59:28) [MSC v.1916 64 bit (AMD64)]
Qt: 5.15.2
PyQt5: 5.15.6
NumPy: 1.21.5
SciPy: 1.7.3
Dask: 2021.11.1
VisPy: 0.10.0

OpenGL:
- GL version: 4.6.0 - Build 27.20.100.9621
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 2736x1824, scale 1.0

Plugins:
- animation: 0.0.2
- console: 0.0.4
- napari-molecule-reader: 0.1.2
- napari-multitask: 0.0.2
- napari-roi: 0.1.3
- napari-svg: 0.1.6
- scikit-image: 0.4.16rc2.dev32+g171e1c74
```
Ah sorry, I can reproduce this on `main` when running the example you gave as a script. But I cannot reproduce it when running in the interactive console in napari. So this should definitely stay open.

I wonder if this is related to #3998.

Resolving patch:
diff --git a/napari/_qt/widgets/qt_dims.py b/napari/_qt/widgets/qt_dims.py
--- a/napari/_qt/widgets/qt_dims.py
+++ b/napari/_qt/widgets/qt_dims.py
@@ -217,6 +217,10 @@ def _remove_slider_widget(self, index):
         slider_widget = self.slider_widgets.pop(index)
         self._displayed_sliders.pop(index)
         self.layout().removeWidget(slider_widget)
+        # As we delete this widget later, callbacks with a weak reference
+        # to it may successfully grab the instance, but may be incompatible
+        # with other update state like dims.
+        self.dims.events.axis_labels.disconnect(slider_widget._pull_label)
         slider_widget.deleteLater()
         nsliders = np.sum(self._displayed_sliders)
         self.setMinimumHeight(int(nsliders * self.SLIDERHEIGHT))


Test patch:
diff --git a/napari/_qt/widgets/_tests/test_qt_dims.py b/napari/_qt/widgets/_tests/test_qt_dims.py
--- a/napari/_qt/widgets/_tests/test_qt_dims.py
+++ b/napari/_qt/widgets/_tests/test_qt_dims.py
@@ -313,3 +313,17 @@ def test_slice_labels(qtbot):
     label_edit.setText(str(8))
     label_edit.editingFinished.emit()
     assert dims.point[0] == 8
+
+
+def test_set_axis_labels_after_ndim_changes(qtbot):
+    """See https://github.com/napari/napari/issues/3753"""
+    dims = Dims(ndim=3, ndisplay=2)
+    view = QtDims(dims)
+    qtbot.addWidget(view)
+
+    dims.ndim = 2
+    dims.axis_labels = ['y', 'x']
+
+    assert len(view.slider_widgets) == 2
+    assert view.slider_widgets[0].axis_label.text() == 'y'
+    assert view.slider_widgets[1].axis_label.text() == 'x'