Repository: napari/napari

Issue:
When using PyQt5 backend on Windows, dragging layer to trash crashes napari
## 🐛 Bug

When dragging a layer to the trash (to delete it), napari crashes. This happens for me on Windows when using `PyQt5`, but does not crash when using `PySide2`. 

I've been meaning to report this for a bit, but thought it was a general Windows trouble until I downloaded the `briefcase` install which uses `PySide2`. I've tested this behavior by installing in a new virtual environment. This used to behave properly for me in the past, but I'm not sure if `napari` changed, or I reinstalled and ended up with `PyQt5` instead. 

In both cases, I get the same traceback (below), but `napari` does not crash if using `PySide2`.

## To Reproduce

Steps to reproduce the behavior:

1. Launch `napari` from command line
2. Open image (dragged image file onto `napari`)
3. Drag image to trash
4. Traceback printed
5. `napari` crashes

This also occurs if there any number of layers. I'm not just deleting the sole layer.

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

Layer is deleted, and `napari` doesn't crash. As if the layer were selected, and the trash can icon is clicked.

## Environment

### No Crash:

```
napari: 0.3.6.dev35+ga119296
Platform: Windows-10-10.0.19041-SP0
Python: 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]
Qt: 5.14.2
PySide2: 5.14.2.3
NumPy: 1.19.0
SciPy: 1.5.1
Dask: 2.21.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 442.70
MAX_TEXTURE_SIZE: 32768

Plugins:
- napari-plugin-engine: 0.1.6
- svg: 0.1.3
```

### Crash

```
napari: 0.3.6.dev35+ga119296
Platform: Windows-10-10.0.19041-SP0
Python: 3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]
Qt: 5.14.2
PyQt5: 5.14.2
NumPy: 1.19.0
SciPy: 1.5.1
Dask: 2.20.0
VisPy: 0.6.4

GL version: 4.6.0 NVIDIA 442.70
MAX_TEXTURE_SIZE: 32768

Plugins:
- napari-plugin-engine: 0.1.6
- svg: 0.1.3
```

## Additional context

Traceback in both cases:
```python
Traceback (most recent call last):
  File "c:\users\hectormz\code\contribute\napari\napari\_qt\qt_layerlist.py", line 307, in mouseMoveEvent
    index = self.layers.index(self._drag_name)
  File "c:\users\hectormz\code\contribute\napari\napari\utils\list\_typed.py", line 82, in index
    raise KeyError(f'could not find element {q} was referencing')
KeyError: 'could not find element stack was referencing'
```



Discussion:
Interesting, we ran into some problems with the recently while working on a PR we didn't end up keeping, but at the time I thought we might just want to drop this behaviour, curious @hectormz if you use it frequently or @jni @tlambert03 if you have thoughts?
I'll say that it is a nice feature, but I mostly learned to stop doing it to avoid the crash. There's some reason that I used to find it easier/more enjoyable to drag and drop. But if it never existed, users might not know to miss it. 

Is there any idea how to prevent the crash? It may not be worth figuring out.

Does napari support PyQt5 and PySide2 to be accommodating of the user's environment? Why should I want to use one over the other?
Yeah, *if it's easy*, I think it's worth creating, as it matches OS patterns. But I don't know how easy it is.

@hectormz 

> Does napari support PyQt5 and PySide2 to be accommodating of the user's environment?

Yes, you can do `pip install napari[pyside2]`, `pip install napari[pyqt5]`, or `pip install napari[all]  # defaults to pyqt5`. That's implemented in these lines:

https://github.com/napari/napari/blob/045e0232199c4dcd072062304a4f39ddb568370b/setup.cfg#L68-L76

Theoretically they should be identical in functionality, but PySide2 has a LGPL license which means we can release the napari bundle without making the whole bundle GPL. But inevitably small differences in functionality creep in.
Also, having both installed can sometimes cause issues, so you might prefer one over the other based on the dependencies of other apps in your env. I think generally PyQt5 is a bit more popular, probably because of its name.
> I'll say that it is a nice feature, but I mostly learned to stop doing it to avoid the crash. 

Ok great, we should look into a fix
@sofroniewn was that #1416 ?
@jni thanks for the explainer!

I'm curious why `PySide2` allows this exception and doesn't kill napari.

If I understand it correctly, the code below handles dragging layers, whether to rearrange, or to drag a layer into the trash

https://github.com/napari/napari/blob/ba705e3924aab235daf35de22d9e3568f4012240/napari/_qt/qt_layerlist.py#L285-L309

But if the layer has been trashed in the drag executed at 305, then a `KeyError` is raised at 307 because the layer no longer exists, and cannot be indexed. I imagine the if-block is no longer relevant if the layer(s) have been deleted. So we should add something that tells us that the layer has been deleted, or to prepare us to consider that it has been deleted.

updating the if block to :

```python
            try:
                index = self.layers.index(self._drag_name)
                layer = self.layers[index]
                self._ensure_visible(layer)
            except KeyError:
                pass
```
is a bandaid for now that avoids the crash
Without having looked too closely, I wonder if this has to do with differences between MimeData handling between the two backends.  I had some issues with this with the layer group tree as well (that I fixed with a new mimedata object).  I'd say let's put the quick fix in there for now, because that file is going to be replaced soon anyway.
> @sofroniewn was that #1416 ?

@hectormz I explored dropping that functionality in #1391 which was reverted in #1416. As @tlambert03 mentioned there will be improvements coming here, so a quick fix is fine for now, but I'd prefer to avoid the try/ except approach and be more explicit.

I think ideally I'd wanted something like
```python
        if self._drag_name is not None and self._drag_name in self.layers:
```
but #306 has not been fixed (see problem here https://github.com/napari/napari/pull/306#issuecomment-497035369).

so instead you could do 
```python
        if self._drag_name is not None and self._drag_name in [l.name fo l in self.layers]:
```

I think that should avoid the crash without needing the `try/ except`
> @sofroniewn: I'd prefer to avoid the try/ except approach

are you a "Look Before You Leap" guy? 😄
This is definitely debated... but some consider the try/except ([EAFP](https://docs.python.org/3.5/glossary.html#term-eafp)) pattern to be the [more explicit idiomatic python](https://devblogs.microsoft.com/python/idiomatic-python-eafp-versus-lbyl/#:~:text=One%20idiomatic%20practice%20in%20Python,ask%20for%20forgiveness%20than%20permission%E2%80%9D.).  (I don't mind the try/except)

> @hectormz: I'm curious why PySide2 allows this exception and doesn't kill napari.

This _may_ be related to the way various qt backends handle uncaught exceptions, and when they do or don't call `qFatal()`.  This isn't related to the issue at hand, but there's a little background in https://github.com/napari/napari/pull/794#issuecomment-647131401, and you may be generally interested in reading through some of the discussions in #794, and #1090

(also note that hopefully neither will totally crash after #1476)
> are you a "Look Before You Leap" guy? 😄

I'm certainly not dogmatic here, I guess my reasoning was in simple cases if find it more readable when things are explicit. I'd defer to @jni, but would say we are mostly doing this sort of checking in the codebase so at least for consistency I'd prefer we stuck with that approach.  
I think the recent consensus in Python-land is that exceptions are for exceptional circumstances. If this is an *expected* code path, you should check for it and follow it, if it's unexpected, then use try/except.
Fascinating:)
Okay, that makes sense to me. @jni's reasoning sounds familiar. For the sake of line length and readability I would opt for something like:

```python
        dragged_layer_exists = self._drag_name in [l.name for l in self.layers]
        if self._drag_name is not None and dragged_layer_exists:
```
LTGM @hectormz, PR would be great

Resolving patch:
diff --git a/napari/_qt/widgets/qt_layerlist.py b/napari/_qt/widgets/qt_layerlist.py
--- a/napari/_qt/widgets/qt_layerlist.py
+++ b/napari/_qt/widgets/qt_layerlist.py
@@ -302,7 +302,10 @@ def mouseMoveEvent(self, event):
         drag.setMimeData(mimeData)
         drag.setHotSpot(event.pos() - self.rect().topLeft())
         drag.exec_()
-        if self._drag_name is not None:
+        # Check if dragged layer still exists or was deleted during drag
+        names = [layer.name for layer in self.layers]
+        dragged_layer_exists = self._drag_name in names
+        if self._drag_name is not None and dragged_layer_exists:
             index = self.layers.index(self._drag_name)
             layer = self.layers[index]
             self._ensure_visible(layer)


Test patch: