Repository: napari/napari

Issue:
Launching two viewers broken
## 🐛 Bug

Unfortunately I think this is a blocking regression for 0.4.11. The following script crashes on master without any error message:

```python
import napari

v1 = napari.Viewer()
v2 = napari.Viewer()

napari.run()
```

I traced the regression to #3034 07f35e652ca136de318eeac5d16717333fdbf432 with git-bisect. Unfortunately I don't have any ideas about how to fix it.

```
napari: 0.4.11rc3
Platform: macOS-11.4-x86_64-i386-64bit
System: MacOS 11.4
Python: 3.9.6 | packaged by conda-forge | (default, Jul 11 2021, 03:36:15)  [Clang 11.1.0 ]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.1
SciPy: 1.7.0
Dask: 2021.07.1
VisPy: 0.7.3

OpenGL:
  - GL version:  2.1 ATI-4.5.14
  - MAX_TEXTURE_SIZE: 16384
```


Discussion:
I'm surprised this wasn't caught by any of our tests! I guess we don't have multi-viewer tests?

I'd like us to give fixing this a try @Carreau @goanpeca any chance you could give this one a look? Thanks!!
I tried both
```
export NAPARI_EXIT_ON_ERROR=1
export NAPARI_CATCH_ERRORS=1
```

and i still just get an exit with no error message which isn't great! I'd hope those would keep working regardless of bug ... I'll keep looking
maybe @nclack wants to dive into this one! just getting a stack trace/ error message would be a big help. it could be a trivial fix, it could be really tricky - no way to know right now. @lukasz-migas might also want to give it a try if he has time
it's failing because it thinks that ipython has the event loop in the call to `run()`... so it bails (and there's no error because that's not technically an error 🤪)

haven't figured out why yet... but it might have to do with updates to napari-console

_edit_: (it doesn't... same with console-0.0.3)
note, this is actually related to #2925 and #3033... (not sure what happened in #3034 to aggravate it)

Resolving patch:
diff --git a/napari/_qt/qt_event_loop.py b/napari/_qt/qt_event_loop.py
--- a/napari/_qt/qt_event_loop.py
+++ b/napari/_qt/qt_event_loop.py
@@ -55,6 +55,7 @@ def set_app_id(app_id):
 
 # store reference to QApplication to prevent garbage collection
 _app_ref = None
+_IPYTHON_WAS_HERE_FIRST = "IPython" in sys.modules
 
 
 def get_app(
@@ -165,7 +166,8 @@ def get_app(
 
     if ipy_interactive is None:
         ipy_interactive = get_settings().application.ipy_interactive
-    _try_enable_ipython_gui('qt' if ipy_interactive else None)
+    if _IPYTHON_WAS_HERE_FIRST:
+        _try_enable_ipython_gui('qt' if ipy_interactive else None)
 
     if perf_config and not perf_config.patched:
         # Will patch based on config file.


Test patch:
diff --git a/napari/_qt/_tests/test_app.py b/napari/_qt/_tests/test_app.py
--- a/napari/_qt/_tests/test_app.py
+++ b/napari/_qt/_tests/test_app.py
@@ -1,8 +1,10 @@
 import os
+from unittest.mock import Mock
 
 import pytest
 
-from napari._qt.qt_event_loop import set_app_id
+from napari import Viewer
+from napari._qt.qt_event_loop import _ipython_has_eventloop, run, set_app_id
 
 
 @pytest.mark.skipif(os.name != "Windows", reason="Windows specific")
@@ -25,3 +27,21 @@ def get_app_id():
     assert get_app_id() == "custom_string"
     set_app_id("")
     assert get_app_id() == ""
+
+
+def test_run_outside_ipython(qapp, monkeypatch):
+    """Test that we don't incorrectly give ipython the event loop."""
+    assert not _ipython_has_eventloop()
+    v1 = Viewer(show=False)
+    assert not _ipython_has_eventloop()
+    v2 = Viewer(show=False)
+    assert not _ipython_has_eventloop()
+
+    with monkeypatch.context() as m:
+        mock_exec = Mock()
+        m.setattr(qapp, 'exec_', mock_exec)
+        run()
+        mock_exec.assert_called_once()
+
+    v1.close()
+    v2.close()