Skip to content

Conversation

@GuillaumeFavelier
Copy link
Contributor

This PR adds support for qtpy which allows us to become Qt-agnostic. This can eventually make the transition to Qt6 easier.

Related to mne-tools/mne-gui-addons#14

@GuillaumeFavelier GuillaumeFavelier self-assigned this Mar 10, 2022
@GuillaumeFavelier
Copy link
Contributor Author

As I explain in mne-tools/mne-gui-addons#14, the resource system needs some rework to allow Qt6 support.

@GuillaumeFavelier
Copy link
Contributor Author

Although there is no pyrcc6, pyside6-rcc does exist:

https://doc.qt.io/qtforpython/tutorials/basictutorial/qrcfiles.html

So I think we could migrate to PySide6 easily but for the sake of being Qt-agnostic, I still plan to rework the resource system.

@larsoner
Copy link
Member

larsoner commented Mar 10, 2022

Could we just load the PNGs/SVGs directly?

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Mar 10, 2022

@larsoner feel free to take a look at the code. This did the trick for me on PySide6, I'll try with PyQt6 next.

Could we just load the PNGs/SVGs directly?

Definitely, but I think we need to be smart here because a relative path won't do. I chose importlib.resources and found that it worked quite well. I just refactored _init_qt_resources().

Of course, the PR is not ready, I tried Brain and coreg mainly.

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Mar 10, 2022

There are still many places in the project where PyQt5 is used or referred to directly:

Details
.circleci/config.yml:            name: Check PyQt5
.circleci/config.yml:            command: LD_DEBUG=libs python -c "from PyQt5.QtWidgets import QApplication, QWidget; app = QApplication([])"
doc/install/mne_python.rst:    PyQt5:         5.14.1
mne/conftest.py:    # Check PyQt5
mne/conftest.py:        import PyQt5  # noqa: F401
mne/conftest.py:        pytest.skip('PyQt5 is not installed but needed for pyqtgraph!')
mne/conftest.py:        pytest.skip(f'PyQt5 has version {_check_pyqt5_version()}'
mne/conftest.py:        pytest.skip("Test skipped, requires PyQt5.")
mne/conftest.py:    from PyQt5.QtWidgets import QApplication, QMainWindow
mne/gui/__init__.py:    from PyQt5.QtWidgets import QApplication
mne/gui/__init__.py:        from PyQt5 import QtGui
mne/gui/_ieeg_locate_gui.py:from PyQt5 import QtCore, QtGui, Qt
mne/gui/_ieeg_locate_gui.py:from PyQt5.QtCore import pyqtSlot
mne/gui/_ieeg_locate_gui.py:from PyQt5.QtWidgets import (QMainWindow, QGridLayout,
mne/utils/check.py:        from PyQt5.Qt import PYQT_VERSION_STR as version
mne/utils/check.py:        warn('macOS users should use PyQt5 >= 5.10 for GUIs, got %s. '
mne/utils/check.py:             '    pip install "PyQt5>=5.10,<5.14"\n'
mne/utils/config.py:        PyQt5:         5.15.0
mne/utils/config.py:                     'PyQt5', 'ipympl', 'pooch', '', 'mne_bids', 'mne_nirs',
mne/utils/config.py:            elif mod_name == 'PyQt5':
mne/viz/_scraper.py:        from PyQt5.QtWidgets import QApplication
mne/viz/backends/tests/_utils.py:    """Check if PyQt5 is installed."""
mne/viz/backends/tests/_utils.py:        import PyQt5  # noqa: F401
mne/viz/raw.py:    fig : matplotlib.figure.Figure | ``PyQt5.QtWidgets.QMainWindow``
mne/viz/utils.py:    """Protect against PyQt5 exiting on event-handling errors."""
mne/viz/utils.py:        from PyQt5.QtWidgets import QApplication
tools/azure_dependencies.sh:    python -m pip install --progress-bar off --upgrade --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt5 PyQt5-sip PyQt5-Qt5
tools/circleci_dependencies.sh:echo "Working around PyQt5 bugs"
tools/github_actions_dependencies.sh:   echo "PyQt5"
tools/github_actions_dependencies.sh:   pip install $STD_ARGS --pre --only-binary ":all:" --no-deps --extra-index-url https://www.riverbankcomputing.com/pypi/simple PyQt5 PyQt5-sip PyQt5-Qt5

Plus a DeprecationWarning:

/home/guillaume/source/mne-python/mne/viz/backends/_qt.py:384: DeprecationWarning: Function: 'pos() const' is marked as deprecated, please check the documentation for more information.
  if (event.button() != Qt.LeftButton or sr.contains(event.pos())):

@larsoner
Copy link
Member

As a first step let's make the only change CIs just to add qtpy to the CI requirements, keeping PyQt5 as the lib that actually gets installed. Then this PR's purpose can be to remove PyQt5 uses in our code in favor of some pyqt abstraction. Once that's green we can merge.

Then the next PR can be to improve CIs by adding PySide2 / PySide 6 / PyQt6 runs -- this will require a bit of work and thinking as we probably only want to test things that use our new abstraction (Brain, iEEG GUI, etc.). The easiest way to do this is maybe by making a pytest.mark.qt or so to signify that it's a Qt test, then we can pytest -m qt in these (hopefully quick) framework-only CI runs. Or maybe we get this for free by using something like pytest-qt with a qtbot fixture, in which case we should just add it to the appropriate set of tests and use it.

@GuillaumeFavelier
Copy link
Contributor Author

Also I think there is no hurry. I jumped on this because I am interested but I can wait for the release.

@larsoner
Copy link
Member

Agreed we should merge this right after release. So actually if the qtpy abstraction is quick and some commit is green before release, it's okay to roll the CI changes into this PR, too. It will be obvious what CIs and infrastructure have been added to make it work I think.

@GuillaumeFavelier
Copy link
Contributor Author

I guess I have to open this PR rather sooner than later 😅

Meanwhile, do you think it's worth patching in pyvistaqt @larsoner ?

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

IIRC VTK 9.2 will already support PyQt6. I think it's supposed to come out in a month. So perhaps for now we should just omit the PyQt6 support / CI entry.

An option in the interim would be to get an updated pyvista wheel with 9.2dev that supports PyQt6. But it doesn't seem worth it.

So that covers PyQt6. Why isn't PySide2 working though?

@GuillaumeFavelier
Copy link
Contributor Author

IIRC VTK 9.2 will already support PyQt6

This is good to know, thank you for the info!

So that covers PyQt6. Why isn't PySide2 working though?

I will update the PR to disable PyQt6 testing then. I'll continue to investigate

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

I will update the PR to disable PyQt6 testing then.

On second thought, let's just leave the PyQt6 CI entry in here. It at least tests matplotlib, and eventually will get mne-qt-browser, then finally VTK. And the skips work in the meantime.

So I think all that is left to do is figure out PySide2...

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

... I can't resist the temptation to try following https://docs.pyvista.org/extras/building_vtk.html#building-wheels in a 20.04 VM to see if I can make suitable wheels here. Will push in a little bit. Feel free to push any commits to fix PySide2 in the meantime...

@GuillaumeFavelier
Copy link
Contributor Author

For PySide2, the error is slightly different:

❯ python
Python 3.9.12 (main, Apr  5 2022, 06:56:58) 
[GCC 7.5.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyvistaqt
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/pyvistaqt/__init__.py", line 31, in <module>
    from .plotting import BackgroundPlotter, MainWindow, MultiPlotter, QtInteractor
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/shiboken2/files.dir/shibokensupport/__feature__.py", line 142, in _import
    return original_import(name, *args, **kwargs)
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/pyvistaqt/plotting.py", line 76, in <module>
    from .rwi import QVTKRenderWindowInteractor
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/shiboken2/files.dir/shibokensupport/__feature__.py", line 142, in _import
    return original_import(name, *args, **kwargs)
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/pyvistaqt/rwi.py", line 143, in <module>
    from PyQt5.QtWidgets import QWidget
  File "/home/guillaume/source/anaconda3/envs/test/lib/python3.9/site-packages/shiboken2/files.dir/shibokensupport/__feature__.py", line 142, in _import
    return original_import(name, *args, **kwargs)
ModuleNotFoundError: No module named 'PyQt5.QtWidgets'
>>> from qtpy import API
>>> API
'pyside2'
>>> 

This time, PySide2 is in the list of vtkmodules.qt but PyQt5 is checked first and for an unknown reason:

import sys

# PyQtImpl can be set by the user
PyQtImpl = None

# Has an implementation has been imported yet?
for impl in ["PySide6", "PyQt5", "PySide2", "PyQt4", "PySide"]:
    if impl in sys.modules:
        PyQtImpl = impl
        break

PyQtImpl is set to PyQt5 even though I uninstalled it 🤔

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

PyQtImpl is set to PyQt5 even though I uninstalled it

If you're on linux, perhaps you have one in the system dist, like python3-pyqt5 or whatever?

@GuillaumeFavelier
Copy link
Contributor Author

I investigated and it is caused by qtpy. Setting QT_API should fix it.

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

Setting QT_API should fix it.

Can you open an upstream bug report with them? In the meantime, let's set this env var in just the PySide2 calls, then. (I can push a commit for this if you don't get what I mean.) And let's reenable PyQt6 -- sorry for the suggestion to disable earlier! Give me 1h to try to get a working VTK 9.2dev build to see if I can get PyQt6 fully working, but even if we don't, we can still test the non-3D stuff with that run...

@GuillaumeFavelier
Copy link
Contributor Author

GuillaumeFavelier commented Apr 6, 2022

Can you open an upstream bug report with them?

Sorry for the noise. Technically, qtpy is right because it tries to import PyQt5 and it properly detects PySide2 afterwards. That's when PyQt5 ends up in sys.modules and then is detected by vtk (because of the order in vtkmodules.qt).

The real problem is that it should not be possible to import PyQt5 after uninstalling it right? Locally, it's still there for me:

python3.9/site-packages/PyQt5 
❯ tree   
.
├── bindings
└── Qt5
    ├── plugins
    ├── qml
    │   ├── Qt
    │   │   ├── labs
    │   │   └── test
    │   └── QtQuick
    └── qsci
        └── api

10 directories, 0 files

When I delete this directory manually, the issue is resolved because PyQt5 is not present in sys.modules (ImportError). Setting QT_API prevents qtpy from importing PyQt5, that's why it also works for me.

I can also guess that it works with PySide6 because it has higher priority than PyQt5 in vtkmodules.qt.

At this point, I don't know if it's only me or this is something related to conda, pip or PyQt5 😅

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

At this point, I don't know if it's only me or this is something related to conda or pip

Does pip list | grep -i pyqt5 yield anything? Maybe there is some other package like https://pypi.org/project/pyqt5-tools/ is installed?

@GuillaumeFavelier
Copy link
Contributor Author

One moment, I will double-check because I modified the env too much 👍

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

... given that this can happen, though, it might still make sense to make a PR to qtpy to go beyond just a in sys.modules check, and actually try to import something that should be there, like QtCore, and only accept it as "imported" if it works.

@GuillaumeFavelier
Copy link
Contributor Author

The check of sys.modules is done in vtk, qtpy is correct and tries to import QtCore, that's why it detects PySide2 in the end:

#10430 (comment)

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

https://gitlab.kitware.com/vtk/vtk/-/blob/master/Wrapping/Python/vtkmodules/qt/__init__.py#L27-31

Argh, I missed that this was already master! It would be good to open a PR to VTK to just add PyQt6 now that 9.2 will support it. They hopefully/maybe just forgot to update this Python code along the way. It's also possible that more changes will be required beyond just adding it, but at least we can get the ball rolling with them by making a PR to add PyQt6 to the list. Feel free to ping me over there (still as @larsoner )

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

qtpy is correct and tries to import QtCore,

I would also port this over to VTK and see what they say. In a worst case we have to remove it and just live with the in sys.modules check, but I assume qtpy did it for a good reason (maybe the one we're hitting here actually...)

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

... FYI here is a little diff I'm going to apply to VTK master and do a manylinux docker wheel build of 9.2dev / master with the following patch applied:

diff --git a/Wrapping/Python/vtkmodules/qt/__init__.py b/Wrapping/Python/vtkmodules/qt/__init__.py
index 2b5b2f73fb..cb7c27a308 100644
--- a/Wrapping/Python/vtkmodules/qt/__init__.py
+++ b/Wrapping/Python/vtkmodules/qt/__init__.py
@@ -19,16 +19,24 @@ For more information, see QVTKRenderWidgetConeExample() in the file
 QVTKRenderWindowInteractor.py.
 """
 
+import importlib
 import sys
 
 # PyQtImpl can be set by the user
 PyQtImpl = None
 
 # Has an implementation has been imported yet?
-for impl in ["PySide6", "PyQt5", "PySide2", "PyQt4", "PySide"]:
+for impl in ["PySide6", "PyQt6", "PyQt5", "PySide2", "PyQt4", "PySide"]:
     if impl in sys.modules:
-        PyQtImpl = impl
-        break
+        # Sometimes an attempted import can be crufty (e.g., unclean
+        # uninstalls of PyQt5), so let's try to import the actual functionality
+        try:
+            importlib.import_module(impl + '.QtCore')
+        except Exception:
+            pass
+        else:
+            PyQtImpl = impl
+            break
 
 # QVTKRWIBase, base class for QVTKRenderWindowInteractor,
 # can be altered by the user to "QGLWidget" or "QOpenGLWidget"

It's probably going to take a couple of hours to actually build, then I'll upload the wheel to OSF, and we can try it here!

@GuillaumeFavelier
Copy link
Contributor Author

It's probably going to take a couple of hours to actually build, then I'll upload the wheel to OSF, and we can try it here!

Thank you for the help! I think the updated test is more robust indeed.

Does pip list | grep -i pyqt5 yield anything?

Sorry for the delay, I purged my pip cache:

mne-python on  mnt/qtpy via 🐍 v3.9.12 via 🅒 test 
❯ pip list | grep -i qt   
mne-qt-browser                0.2.6
pyqtgraph                     0.12.4
pyvistaqt                     0.8.0
qtconsole                     5.3.0
QtPy                          2.0.1
sphinxcontrib-qthelp          1.0.3
mne-python on  mnt/qtpy via 🐍 v3.9.12 via 🅒 test 
❯ pip list | grep -i pyside2
PySide2                       5.15.2.1

@larsoner
Copy link
Member

larsoner commented Apr 6, 2022

@GuillaumeFavelier looks like the PyQt6 failure is real, can you look?

https://dev.azure.com/mne-tools/mne-python/_build/results?buildId=18893&view=logs&jobId=2b5832ae-6860-5681-a4e1-fd132048f8b4&j=2b5832ae-6860-5681-a4e1-fd132048f8b4&t=c756476c-2b4b-5513-f263-3096f03430e2

Only one failure, though!

@GuillaumeFavelier
Copy link
Contributor Author

At the moment, there are some timeouts for:

mne-python on  mnt/qtpy via 🐍 v3.9.7 via 🅒 pyside2 took 18s 
❯ pip install -i "https://pypi.anaconda.org/scipy-wheels-nightly/simple" pandas            
Looking in indexes: https://pypi.anaconda.org/scipy-wheels-nightly/simple
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ReadTimeoutError("HTTPSConnectionPool(host='pypi.anaconda.org', port=443): Read timed out. (read timeout=15)")': /scipy-wheels-nightly/simple/pandas/

I will try again later.

@larsoner larsoner merged commit af68533 into mne-tools:main Apr 7, 2022
@larsoner
Copy link
Member

larsoner commented Apr 7, 2022

Thanks @GuillaumeFavelier !

@GuillaumeFavelier GuillaumeFavelier deleted the mnt/qtpy branch April 8, 2022 03:32
@larsoner larsoner mentioned this pull request Apr 8, 2022
larsoner added a commit to wmvanvliet/mne-python that referenced this pull request Apr 19, 2022
* upstream/main: (40 commits)
  FIX: Flake (mne-tools#10540)
  FIX: Correct link (mne-tools#10536)
  DOC: Update installers (mne-tools#10535)
  ENH: Add dark mode to website (mne-tools#10523)
  WIP: Copy BEM surfaces by default (don't symlink) (mne-tools#10531)
  Avoid lowpass=0 in brainvision data (mne-tools#10517)
  DOC: Update installers [skip azp] [skip actions] (mne-tools#10528)
  FIX: Fix for old build (mne-tools#10527)
  Fix line noise at wrong frequencies (mne-tools#10525)
  FIX : read fids in eeglab (mne-tools#10521)
  MAINT: Prefer PySide6 in testing (mne-tools#10513)
  ENH: Add overview_mode support (mne-tools#10501)
  MRG: Updates for qtpy in mne-qt-browser (mne-tools#10509)
  BUG: Fix bug with themes on macOS (mne-tools#10500)
  MAINT: Bump installer links (mne-tools#10511)
  Add metadata to combine_channels (mne-tools#10504)
  MAINT: Standardize tests (mne-tools#10502)
  CI: Test circle (mne-tools#10506)
  ENH: Use HiDPI splash screen on HiDPI screens (mne-tools#10503)
  WIP,MNT: Add support for QtPy (mne-tools#10430)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants