Repository: napari/napari

Issue:
`viewer.reset()` throws `TypeError`
## 🐛 Bug
Calling `viewer.reset()` via the napari console results in a `TypeError`.


## To Reproduce

Steps to reproduce the behavior:

1. Open an image e.g. XYZ tif.
2. Open iPython console.
3. Run `viewer.reset()`.

```
viewer.reset()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [1], in <cell line: 1>()
----> 1 viewer.reset()

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/evented_model.py:253, in EventedModel.reset(self)
    251 for name, value in self._defaults.items():
    252     if isinstance(value, EventedModel):
--> 253         getattr(self, name).reset()
    254     elif (
    255         self.__config__.allow_mutation
    256         and self.__fields__[name].field_info.allow_mutation
    257     ):
    258         setattr(self, name, value)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/evented_model.py:253, in EventedModel.reset(self)
    251 for name, value in self._defaults.items():
    252     if isinstance(value, EventedModel):
--> 253         getattr(self, name).reset()
    254     elif (
    255         self.__config__.allow_mutation
    256         and self.__fields__[name].field_info.allow_mutation
    257     ):
    258         setattr(self, name, value)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/evented_model.py:258, in EventedModel.reset(self)
    253     getattr(self, name).reset()
    254 elif (
    255     self.__config__.allow_mutation
    256     and self.__fields__[name].field_info.allow_mutation
    257 ):
--> 258     setattr(self, name, value)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/evented_model.py:234, in EventedModel.__setattr__(self, name, value)
    232 are_equal = self.__eq_operators__.get(name, operator.eq)
    233 if not are_equal(after, before):
--> 234     getattr(self.events, name)(value=after)  # emit event
    236     # emit events for any dependent computed property setters as well
    237     for dep in self.__field_dependents__.get(name, {}):

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/event.py:757, in EventEmitter.__call__(self, *args, **kwargs)
    754     self._block_counter.update([cb])
    755     continue
--> 757 self._invoke_callback(cb, event if pass_event else None)
    758 if event.blocked:
    759     break

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/event.py:794, in EventEmitter._invoke_callback(self, cb, event)
    792     self.disconnect(cb)
    793     return
--> 794 _handle_exception(
    795     self.ignore_callback_errors,
    796     self.print_callback_errors,
    797     self,
    798     cb_event=(cb, event),
    799 )

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/event.py:781, in EventEmitter._invoke_callback(self, cb, event)
    779 try:
    780     if event is not None:
--> 781         cb(event)
    782     else:
    783         cb()

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/components/_interaction_box_mouse_bindings.py:192, in InteractionBoxMouseBindings._on_transform_change(self, event)
    184 """Gets called when the interaction box is transformed to update transform of the layer"""
    186 layer_dims_displayed = dims_displayed_world_to_layer(
    187     list(self._ref_viewer().dims.displayed),
    188     self._ref_viewer().dims.ndim,
    189     self._ref_viewer().layers.selection.active.ndim,
    190 )
--> 192 layer_affine_transform = event.value.compose(
    193     self._initial_transform_inverse
    194 )
    196 self._ref_viewer().layers.selection.active.affine = (
    197     self._ref_viewer().layers.selection.active.affine.replace_slice(
    198         layer_dims_displayed, layer_affine_transform
    199     )
    200 )

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/transforms/transforms.py:552, in Affine.compose(self, transform)
    550 """Return the composite of this transform and the provided one."""
    551 if not isinstance(transform, Affine):
--> 552     return super().compose(transform)
    553 affine_matrix = self.affine_matrix @ transform.affine_matrix
    554 return Affine(affine_matrix=affine_matrix)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/transforms/transforms.py:60, in Transform.compose(self, transform)
     58 def compose(self, transform: 'Transform') -> 'Transform':
     59     """Return the composite of this transform and the provided one."""
---> 60     return TransformChain([self, transform])

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/transforms/transforms.py:116, in TransformChain.__init__(self, transforms)
    114 if transforms is None:
    115     transforms = []
--> 116 super().__init__(
    117     data=transforms,
    118     basetype=Transform,
    119     lookup={str: lambda x: x.name},
    120 )
    121 # The above super().__init__() will not call Transform.__init__().
    122 # For that to work every __init__() called using super() needs to
    123 # in turn call super().__init__(). So we call it explicitly here.
    124 Transform.__init__(self)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/containers/_evented_list.py:101, in EventedList.__init__(self, data, basetype, lookup)
     98 else:
     99     # otherwise create a new one
    100     self.events = EmitterGroup(source=self, **_events)
--> 101 super().__init__(data, basetype=basetype, lookup=lookup)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/containers/_typed.py:64, in TypedMutableSequence.__init__(self, data, basetype, lookup)
     60 self._basetypes = (
     61     basetype if isinstance(basetype, Sequence) else (basetype,)
     62 )
     63 self._lookup = lookup.copy()
---> 64 self.extend(data)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/_collections_abc.py:983, in MutableSequence.extend(self, values)
    981     values = list(values)
    982 for v in values:
--> 983     self.append(v)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/_collections_abc.py:962, in MutableSequence.append(self, value)
    960 def append(self, value):
    961     'S.append(value) -- append value to the end of the sequence'
--> 962     self.insert(len(self), value)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/containers/_evented_list.py:182, in EventedList.insert(self, index, value)
    180 """Insert ``value`` before index."""
    181 self.events.inserting(index=index)
--> 182 super().insert(index, value)
    183 self.events.inserted(index=index, value=value)
    184 self._connect_child_emitters(value)

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/containers/_typed.py:102, in TypedMutableSequence.insert(self, index, value)
    101 def insert(self, index: int, value: _T):
--> 102     self._list.insert(index, self._type_check(value))

File ~/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/napari/utils/events/containers/_typed.py:160, in TypedMutableSequence._type_check(self, e)
    156 def _type_check(self, e: Any) -> _T:
    157     if self._basetypes and not any(
    158         isinstance(e, t) for t in self._basetypes
    159     ):
--> 160         raise TypeError(
    161             trans._(
    162                 'Cannot add object with type {dtype!r} to TypedList expecting type {basetypes!r}',
    163                 deferred=True,
    164                 dtype=type(e),
    165                 basetypes=self._basetypes,
    166             )
    167         )
    168     return e

TypeError: Cannot add object with type <class 'NoneType'> to TypedList expecting type (<class 'napari.utils.transforms.transforms.Transform'>,)
/home/tibuch/Apps/miniconda3/envs/psf-analysis/lib/python3.8/site-packages/ipykernel/kernelbase.py:757: RuntimeWarning: coroutine 'InProcessKernel._abort_queues' was never awaited!
```

## Expected behavior
Reset the viewer perspective.

## Environment

 - Please copy and paste the information at napari info option in help menubar here:
```
napari: 0.4.16
Platform: Linux-5.14.0-1038-oem-x86_64-with-glibc2.17
System: Ubuntu 22.04 LTS
Python: 3.8.13 (default, Mar 28 2022, 11:38:47) [GCC 7.5.0]
Qt: 5.15.2
PyQt5: 5.15.7
NumPy: 1.23.1
SciPy: 1.8.1
Dask: 2022.7.1
VisPy: 0.10.0

OpenGL:
- GL version: 4.6.0 NVIDIA 470.129.06
- MAX_TEXTURE_SIZE: 32768

Screens:
- screen 1: resolution 3440x1440, scale 1.0
- screen 2: resolution 1920x1080, scale 1.0

Plugins:
- bfio: 2.3.0
- console: 0.0.4
- napari-aicsimageio: 0.7.1
- napari-psf-analysis: 0.2.0
- napari-svg: 0.1.6
- ome-types: 0.3.0
- scikit-image: 0.4.16
```

 - Any other relevant information:

## Additional context
N/A



Discussion:

Resolving patch:
diff --git a/napari/components/_interaction_box_mouse_bindings.py b/napari/components/_interaction_box_mouse_bindings.py
--- a/napari/components/_interaction_box_mouse_bindings.py
+++ b/napari/components/_interaction_box_mouse_bindings.py
@@ -183,20 +183,25 @@ def _on_mode_change(self, event):
     def _on_transform_change(self, event):
         """Gets called when the interaction box is transformed to update transform of the layer"""
 
+        viewer = self._ref_viewer()
+        active_layer = viewer.layers.selection.active
+        if active_layer is None:
+            return
+
         layer_dims_displayed = dims_displayed_world_to_layer(
             list(self._ref_viewer().dims.displayed),
-            self._ref_viewer().dims.ndim,
-            self._ref_viewer().layers.selection.active.ndim,
+            viewer.dims.ndim,
+            active_layer.ndim,
         )
 
-        layer_affine_transform = event.value.compose(
-            self._initial_transform_inverse
+        layer_affine_transform = (
+            event.value.compose(self._initial_transform_inverse)
+            if self._initial_transform_inverse is not None
+            else event.value
         )
 
-        self._ref_viewer().layers.selection.active.affine = (
-            self._ref_viewer().layers.selection.active.affine.replace_slice(
-                layer_dims_displayed, layer_affine_transform
-            )
+        active_layer.affine = active_layer.affine.replace_slice(
+            layer_dims_displayed, layer_affine_transform
         )
 
     def initialize_key_events(self, viewer):
diff --git a/napari/utils/events/containers/_evented_list.py b/napari/utils/events/containers/_evented_list.py
--- a/napari/utils/events/containers/_evented_list.py
+++ b/napari/utils/events/containers/_evented_list.py
@@ -22,6 +22,7 @@
 cover this in test_evented_list.py)
 """
 
+import contextlib
 import logging
 from typing import Callable, Dict, Iterable, List, Sequence, Tuple, Type, Union
 
@@ -190,10 +191,8 @@ def insert(self, index: int, value: _T):
     def _reemit_child_event(self, event: Event):
         """An item in the list emitted an event.  Re-emit with index"""
         if not hasattr(event, 'index'):
-            try:
+            with contextlib.suppress(ValueError):
                 setattr(event, 'index', self.index(event.source))
-            except ValueError:
-                pass
         # reemit with this object's EventEmitter of the same type if present
         # otherwise just emit with the EmitterGroup itself
         getattr(self.events, event.type, self.events)(event)


Test patch:
diff --git a/napari/_tests/test_viewer.py b/napari/_tests/test_viewer.py
--- a/napari/_tests/test_viewer.py
+++ b/napari/_tests/test_viewer.py
@@ -357,3 +357,22 @@ def test_current_viewer(make_napari_viewer):
 
     assert current_viewer() is viewer1
     assert current_viewer() is not viewer2
+
+
+def test_reset_empty(make_napari_viewer):
+    """
+    Test that resetting an empty viewer doesn't crash
+    https://github.com/napari/napari/issues/4867
+    """
+    viewer = make_napari_viewer()
+    viewer.reset()
+
+
+def test_reset_non_empty(make_napari_viewer):
+    """
+    Test that resetting a non-empty viewer doesn't crash
+    https://github.com/napari/napari/issues/4867
+    """
+    viewer = make_napari_viewer()
+    viewer.add_points([(0, 1), (2, 3)])
+    viewer.reset()