Repository: napari/napari

Issue:
CI Failure on Py3.9 PySide6
## 🐛 Bug

I'm seeing this failure on #5216, and can't find any issues reporting it. The test suite fails at test discovery time with the following traceback:

<details>

<summary>

```pytb
  E   ValueError: <enum 'NotificationSeverity'> may only be called with a `str` or an instance of <enum 'NotificationSeverity'>. Got <class 'shibokensupport.enum_310.auto'>
```

</summary>

```pytb
  ________________________ ERROR collecting test session _________________________
  /opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/importlib/__init__.py:127: in import_module
      return _bootstrap._gcd_import(name[level:], package, level)
  <frozen importlib._bootstrap>:1030: in _gcd_import
      ???
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/conftest.py:51: in <module>
      from napari.components import LayerList
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/components/__init__.py:19: in <module>
      from .layerlist import LayerList
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/components/layerlist.py:9: in <module>
      from ..layers import Layer
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/layers/__init__.py:12: in <module>
      from .base import Layer
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/layers/base/__init__.py:1: in <module>
      from .base import Layer, no_op
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/layers/base/base.py:28: in <module>
      from ...utils.key_bindings import KeymapProvider
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/utils/key_bindings.py:46: in <module>
      from ..settings import get_settings
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/settings/__init__.py:6: in <module>
      from ._napari_settings import CURRENT_SCHEMA_VERSION, NapariSettings
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/settings/_napari_settings.py:10: in <module>
      from ._application import ApplicationSettings
  <frozen importlib._bootstrap>:1007: in _find_and_load
      ???
  <frozen importlib._bootstrap>:986: in _find_and_load_unlocked
      ???
  <frozen importlib._bootstrap>:680: in _load_unlocked
      ???
  /tmp/.tox/py39-linux-pyside6/lib/python3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
      exec(co, module.__dict__)
  napari/settings/_application.py:20: in <module>
      class ApplicationSettings(EventedModel):
  napari/utils/events/evented_model.py:82: in __new__
      cls = super().__new__(mcs, name, bases, namespace, **kwargs)
  pydantic/main.py:198: in pydantic.main.ModelMetaclass.__new__
      ???
  pydantic/fields.py:506: in pydantic.fields.ModelField.infer
      ???
  pydantic/fields.py:436: in pydantic.fields.ModelField.__init__
      ???
  pydantic/fields.py:546: in pydantic.fields.ModelField.prepare
      ???
  pydantic/fields.py:570: in pydantic.fields.ModelField._set_default_and_type
      ???
  pydantic/fields.py:439: in pydantic.fields.ModelField.get_default
      ???
  pydantic/utils.py:695: in pydantic.utils.smart_deepcopy
      ???
  /opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/copy.py:172: in deepcopy
      y = _reconstruct(x, memo, *rv)
  /opt/hostedtoolcache/Python/3.9.14/x64/lib/python3.9/copy.py:264: in _reconstruct
      y = func(*args)
  napari/utils/misc.py:279: in __call__
      raise ValueError(
  E   ValueError: <enum 'NotificationSeverity'> may only be called with a `str` or an instance of <enum 'NotificationSeverity'>. Got <class 'shibokensupport.enum_310.auto'>
```

</details>

## To Reproduce

Here is a link to the failed run:

https://github.com/napari/napari/actions/runs/3247412851/jobs/5327346396#step:8:187

I'm not sure yet whether it is sporadic...


Discussion:
Yeah, I got it as well:
https://github.com/napari/napari/actions/runs/3245198568/jobs/5322414844
@czaki had some input re: possibly blocking this version:
https://github.com/napari/napari/pull/5213#issuecomment-1278186737
Ah great, sounds like I get the go-ahead from @Czaki to ignore it for the purposes of the PR 😂 

Thank you for the link @psobolewskiPhD! Very reassuring! Like when the power goes out for the whole street vs just your own house 😂 
Short investigation of problem. 
The source of the problem is that PySide6 is patching the enum module with its implementations. 

But `napari.utils.misc`is imported before PySide6 and `napari.utils.notifications` is imported after. 

So `auto` when creating `NotificationSeverity` is different from auto recognized by `EnumMeta` known in module `napari.utils.misc`.

I see two possible solutions:

1) Auto should be imported from `napari.utils.misc` not `enum`
2) overwrite `__new__` method of `StringEnumMeta` and in implement heuristics to replace PySide6 `auto` with `enum` auto. 

@jni ?

@Czaki sorry can you clarify: are you saying that PySide6 monkey-patches the Python built-in module `enum`??? And if so, is that not reason enough to never use PySide6??? (And/or ask them to stop doing this???)

Anyway, I like approach 1 better as it seems much simpler.
(But next to the import there should be a big comment explaining the situation.)
> @Czaki sorry can you clarify: are you saying that PySide6 monkey-patches the Python built-in module `enum`???

Yes:

```bash
$ cat py_side_6.py 
import PySide6 
from enum import auto

print(auto)
$ python py_side_6.py  
<class 'shibokensupport.enum_310.auto'> 
```

```bash
$ cat py_side_6_r.py
from enum import auto
import PySide6 

print(auto)
$ python py_side_6_r.py
<class 'enum.auto'> 
```

It looks like it affects python bellow 3.10, so we could add conditional dependency based on the python version. 

Here report in their bug tracker: https://bugreports.qt.io/browse/PYSIDE-2063?jql=project%20%3D%20PYSIDE%20AND%20text%20~%20enum

Some person test only on python 3.10 and close...  

Current state:
"It is not a bug, it is a feature."
Next update:

https://codereview.qt-project.org/c/pyside/pyside-setup/+/439654

It looks like it will be solved, but I would like to highlight this part of message:

> It would be nicer to embed enums because of safety against
tampering, but the user complaints will hopefully go away now.


Resolving patch:
diff --git a/napari/__main__.py b/napari/__main__.py
--- a/napari/__main__.py
+++ b/napari/__main__.py
@@ -436,10 +436,6 @@ def _maybe_rerun_with_macos_fixes():
        This requires relaunching the app from a symlink to the
        desired python executable, conveniently named 'napari'.
     """
-    from napari._qt import API_NAME
-
-    # This import mus be here to raise exception about PySide6 problem
-
     if sys.platform != "darwin":
         return
 
@@ -455,6 +451,8 @@ def _maybe_rerun_with_macos_fixes():
     import subprocess
     from tempfile import mkdtemp
 
+    from qtpy import API_NAME
+
     # In principle, we will relaunch to the same python we were using
     executable = sys.executable
     cwd = Path.cwd()
diff --git a/napari/_qt/__init__.py b/napari/_qt/__init__.py
--- a/napari/_qt/__init__.py
+++ b/napari/_qt/__init__.py
@@ -6,7 +6,7 @@
 from ..utils.translations import trans
 
 try:
-    from qtpy import API_NAME, QT_VERSION, QtCore
+    from qtpy import API_NAME, QtCore
 except Exception as e:
     if 'No Qt bindings could be found' in str(e):
         raise type(e)(
@@ -27,17 +27,6 @@
         Path(PySide2.__file__).parent / 'Qt' / 'plugins'
     )
 
-if API_NAME == 'PySide6' and sys.version_info[:2] < (3, 10):
-    from packaging import version
-
-    if version.parse(QT_VERSION) > version.parse("6.3.1"):
-        raise RuntimeError(
-            trans._(
-                "Napari is not expected to work with PySide6 >= 6.3.2 on Python < 3.10",
-                deferred=True,
-            )
-        )
-
 
 # When QT is not the specific version, we raise a warning:
 if tuple(int(x) for x in QtCore.__version__.split('.')[:3]) < (5, 12, 3):
diff --git a/napari/_vispy/__init__.py b/napari/_vispy/__init__.py
--- a/napari/_vispy/__init__.py
+++ b/napari/_vispy/__init__.py
@@ -1,11 +1,10 @@
 import logging
 
+import qtpy
 from vispy import app
 
-from napari._qt import API_NAME
-
 # set vispy application to the appropriate qt backend
-app.use_app(API_NAME)
+app.use_app(qtpy.API_NAME)
 del app
 
 # set vispy logger to show warning and errors only


Test patch:
diff --git a/.github/workflows/test_pull_requests.yml b/.github/workflows/test_pull_requests.yml
--- a/.github/workflows/test_pull_requests.yml
+++ b/.github/workflows/test_pull_requests.yml
@@ -80,10 +80,6 @@ jobs:
           - python: 3.9
             platform: ubuntu-latest
             backend: pyside6
-# uncoment when new qtpy is released
-#          - python: '3.10'
-#            platform: ubuntu-latest
-#            backend: pyside6
 
     steps:
       - name: Cancel Previous Runs