Repository: napari/napari

Issue:
Fix magicgui catching the wrong exception when layer does not exist.
# Description
After update 0.4.1, magicgui fails when checking whether a layers exists already before creating it. It seems that the cause is the update to `LayerList`, which now raises a `ValueError` instead of a `KeyError` when the "result" layer is missing.
This fix seems to be enough, but I'm not sure if these are all the affected places.

## Type of change
- [x] Bug-fix (non-breaking change which fixes an issue)

# How has this been tested?
- [x] all tests pass with my change

## Final checklist:
- [ ] My PR is the minimum possible work for the desired functionality
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix is effective or that my feature works



Discussion:
ah, yeah, thanks @brisvag ... looks like this got in there with the transition from the old list model to the new one in #1504.  It's a tough call actually what the appropriate error should be!  From the syntax `layer_list['non existent layer']` feels like we're looking into a dict, which would raise `KeyError`, but really, it's a failure of `list.index` in `layer_name_array.index('non existent layer')` ... which should be a `ValueError`.
So we should make sure everyone is happy having this be a `ValueError` before we move on it.  I'm torn, and would be happy with either.
> It's a tough call actually what the appropriate error should be! From the syntax layer_list['non existent layer'] feels like we're looking into a dict, which would raise KeyError, but really, it's a failure of list.index in layer_name_array.index('non existent layer') ... which should be a ValueError.
So we should make sure everyone is happy having this be a ValueError before we move on it. I'm torn, and would be happy with either.

I could go either way too, maybe we defer to @jni?

Do you have a sense of how widespread this problem will be @tlambert03? It seems to be effecting all my magicgui demos which take a layer (which is all of them), but not sure if that's something about how I set my demos up.
> Do you have a sense of how widespread this problem will be @tlambert03? It seems to be effecting all my magicgui demos which take a layer (which is all of them), but not sure if that's something about how I set my demos up.

it'll be everywhere... if someone annotates the return value as `Image`, this is basically all the code that gets called... so that error is pretty central

https://github.com/napari/napari/blob/7663e4e4077761ee8a66ee69fe95f9b645f2d828/napari/utils/_magicgui.py#L171-L175
> it'll be everywhere... if someone annotates the return value as Image, this is basically all the code that gets called... so that error is pretty central

Ok then I think it will be nice to try and get an `0.4.2` release out tonight/ tomorrow before the imageJ and friends conference on monday. Should be very doable
It is possible to add a simple test to this PR, to prevent such behavior in the future? 
I think it should raise a KeyError. I think the "correct" way is for getitem to catch the valueerror and then "raise KeyError(msg) from caught_error". 

Resolving patch:
diff --git a/napari/utils/events/containers/_typed.py b/napari/utils/events/containers/_typed.py
--- a/napari/utils/events/containers/_typed.py
+++ b/napari/utils/events/containers/_typed.py
@@ -119,7 +119,33 @@ def __getitem__(self, key: slice) -> 'TypedMutableSequence[_T]':  # noqa
         ...  # pragma: no cover
 
     def __getitem__(self, key):  # noqa: F811
-        _key = self.index(key) if type(key) in self._lookup else key
+        """Get an item from the list
+
+        Parameters
+        ----------
+        key : int, slice, or any type in self._lookup
+            The key to get.
+
+        Returns
+        -------
+        The value at `key`
+
+        Raises
+        ------
+        IndexError:
+            If ``type(key)`` is not in ``self._lookup`` (usually an int, like a regular
+            list), and the index is out of range.
+        KeyError:
+            If type(key) is in self._lookup and the key is not in the list (after)
+            applying the self._lookup[key] function to each item in the list
+        """
+        if type(key) in self._lookup:
+            try:
+                _key = self.index(key)
+            except ValueError as e:
+                raise KeyError(str(e)) from e
+        else:
+            _key = key
         result = self._list[_key]
         return self.__newlike__(result) if isinstance(result, list) else result
 


Test patch:
diff --git a/napari/utils/_tests/test_magicgui.py b/napari/utils/_tests/test_magicgui.py
new file mode 100644
--- /dev/null
+++ b/napari/utils/_tests/test_magicgui.py
@@ -0,0 +1,64 @@
+import sys
+
+import numpy as np
+import pytest
+
+from napari.layers import Image, Labels, Layer, Points
+
+magicgui = pytest.importorskip('magicgui', reason='please install magicgui.')
+
+
+def test_magicgui_returns_image(make_test_viewer):
+    """make sure a magicgui function returning Image adds an Image."""
+
+    @magicgui.magicgui
+    def add_image() -> Image:
+        return np.random.rand(10, 10)
+
+    viewer = make_test_viewer()
+    viewer.window.add_dock_widget(add_image.Gui())
+    assert len(viewer.layers) == 0
+    add_image()  # should add a new layer to the list
+    assert len(viewer.layers) == 1
+    assert viewer.layers[0].name == 'add_image result'
+
+    add_image()  # should just update existing layer on subsequent calls
+    assert len(viewer.layers) == 1
+    assert viewer.layers[0].name == 'add_image result'
+    assert isinstance(viewer.layers[0], Image)
+
+
+def test_magicgui_returns_label(make_test_viewer):
+    """make sure a magicgui function returning Labels adds a Labels."""
+
+    @magicgui.magicgui
+    def add_labels() -> Labels:
+        return np.random.rand(10, 10)
+
+    viewer = make_test_viewer()
+    viewer.window.add_dock_widget(add_labels.Gui())
+    assert len(viewer.layers) == 0
+    add_labels()  # should add a new layer to the list
+    assert len(viewer.layers) == 1
+    assert viewer.layers[0].name == 'add_labels result'
+    assert isinstance(viewer.layers[0], Image)
+
+
+@pytest.mark.skipif(sys.platform == "darwin", reason="segfault on mac CI")
+def test_magicgui_returns_layer_tuple(make_test_viewer):
+    """make sure a magicgui function returning Layer adds the right type."""
+
+    @magicgui.magicgui
+    def add_layer() -> Layer:
+        return [(np.random.rand(10, 3), {'size': 20, 'name': 'foo'}, 'points')]
+
+    viewer = make_test_viewer()
+    viewer.window.add_dock_widget(add_layer.Gui())
+    assert len(viewer.layers) == 0
+
+    add_layer()  # should add a new layer to the list
+    assert len(viewer.layers) == 1
+    layer = viewer.layers[0]
+    assert layer.name == 'foo'
+    assert isinstance(layer, Points)
+    assert layer.data.shape == (10, 3)
diff --git a/requirements/test.txt b/requirements/test.txt
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -9,3 +9,4 @@ fsspec
 zarr
 scikit-image
 pooch
+magicgui
\ No newline at end of file