Qt6/PySide6 compatibility + cleanup (re-apply of #439)#461
Merged
Conversation
Re-implements PR #439 (pyside6_and_cleanup) on top of current master, which had diverged enough to make the original branch unmergeable. - Default type checking and pytest qt_api switched to PySide6; runtime keeps qtpy backend abstraction (PyQt5/PyQt6/PySide2/PySide6 all supported). - plottr/__init__.py exports QAction/QActionGroup (moved to QtGui in Qt6) and PYSIDE6/PYQT6/API_NAME flags; QAbstractItemModel exec_()->exec(). - Per-file PySide6 fixes across apps/plot/data/gui/node modules. - Removed old Sphinx docs, readthedocs config, and legacy prototyping/gui test scripts. - CI: install .[pyside6]; use tlambert03/setup-qt-libs for headless Qt libs. Kept newer master tooling (mypy 2.1.0, checkout@v7, setup-python@v6.3.0) and skipped changes already on master (numpy mypy plugin removal). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Re-applies the Qt6/PySide6 compatibility work (originally in #439) onto current master, updating runtime Qt API usage, CI defaults, and removing legacy docs/prototyping assets.
Changes:
- Switched default CI/testing Qt backend to PySide6 (deps, pytest-qt config, CI setup) and added a
pyside6extra. - Updated core/app code for Qt6 API differences (e.g.,
exec()usage, QAction/QActionGroup module move, nested enums, PySide6 stub compatibility). - Removed legacy docs and manual/prototyping scripts not covered by the maintained pytest suite.
Reviewed changes
Copilot reviewed 66 out of 68 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/scripts/h5py_concurrent_rw_swmr.py | Removed legacy manual concurrency script. |
| test/scripts/h5py_concurrent_rw_lock.py | Removed legacy manual concurrency script. |
| test/run_gui_test.py | Removed legacy GUI test runner script. |
| test/pytest/test_ddh5.py | Adjusted loader-node test timing/Qt handling. |
| test/pytest/test_app_manager.py | Added note about a locally flaky test. |
| test/pytest/pytest.ini | Default pytest-qt backend set to PySide6. |
| test/prototyping/test_data.py | Removed prototyping helper script. |
| test/prototyping/plottrcfg_main.py | Removed prototyping config snippet. |
| test/prototyping/new_data_methods.ipynb | Removed prototyping notebook. |
| test/prototyping/autoplot testing.ipynb | Removed prototyping notebook. |
| test/gui/simple_2d_plot.py | Removed manual GUI demo script. |
| test/gui/pyqtgraph_testing.py | Removed manual GUI demo script. |
| test/gui/pyqtgraph_figuremaker.py | Removed manual GUI demo script. |
| test/gui/mpl_figuremaker.py | Removed manual GUI demo script. |
| test/gui/grid_options.py | Removed manual GUI demo script. |
| test/gui/dimension_selection_widgets.py | Removed manual GUI demo script. |
| test/gui/dimension_assignment.py | Removed manual GUI demo script. |
| test/gui/ddh5_loader.py | Removed manual GUI demo script. |
| test/gui/data_selector.py | Removed manual GUI demo script. |
| test/gui/data_display_widgets.py | Removed manual GUI demo script. |
| test/gui/correct_offset.py | Removed manual GUI demo script. |
| test/apps/test_histograms.py | Removed manual app/prototyping script. |
| test/apps/fitter_test.py | Removed manual app/prototyping script. |
| test/apps/custom_app.py | Removed manual app/prototyping script. |
| test/apps/autoplot_app.py | Removed manual app/prototyping script. |
| test_requirements.txt | Switched Qt stub package to PySide6-stubs. |
| readthedocs.yml | Removed RTD config (docs cleanup). |
| pyproject.toml | Added pyside6 extra + mypy override for Qt enum typing differences. |
| plottr/plot/pyqtgraph/autoplot.py | Migrated QAction/QActionGroup usage for Qt6. |
| plottr/node/fitter.py | Relaxed PyQt-specific signal typing for PySide6. |
| plottr/gui/data_display.py | Fixed selection-mode enum access for PySide6/Qt6. |
| plottr/data/datadict.py | Made _DataAccess.__getattribute__ robust to missing _parent (pickle/unpickle). |
| plottr/data/datadict_storage.py | Added slot typing ignore for PySide6 stubs. |
| plottr/apps/watchdog_classes.py | Removed redundant mypy ignores after config change. |
| plottr/apps/ui/monitr.py | Added slot typing ignore for PySide6 stubs. |
| plottr/apps/monitr.py | Qt6 compatibility changes + UI bugfixes/guards (exec/QAction/etc). |
| plottr/apps/json_viewer.py | Routed Qt imports through plottr binding + Qt6 enum/signal adjustments. |
| plottr/apps/inspectr.py | Qt6 QAction + exec() updates + PySide6 QTreeWidgetItem init fix. |
| plottr/apps/autoplot.py | Qt6 QAction + exec() updates. |
| plottr/apps/apprunner.py | Qt6 exec() usage. |
| plottr/apps/appmanager.py | PySide6 QProcess state enum handling + slot typing ignores + typo fix. |
| plottr/init.py | Centralized Qt binding exports (Signal/Slot/QAction/QActionGroup) + Qt6 exec(). |
| doc/requirements.txt | Removed Sphinx doc requirements (docs cleanup). |
| doc/plotnode.rst | Removed legacy Sphinx docs content. |
| doc/nodes/index.rst | Removed legacy Sphinx docs content. |
| doc/Makefile | Removed legacy Sphinx build file. |
| doc/make.bat | Removed legacy Sphinx build file. |
| doc/intro.rst | Removed legacy Sphinx docs content. |
| doc/index.rst | Removed legacy Sphinx docs content. |
| doc/examples/Simple Live plotting example with DDH5.ipynb | Removed legacy docs example notebook. |
| doc/examples/node_with_dimension_selector_widget.py | Removed legacy docs example script. |
| doc/examples/Live plotting qcodes data.ipynb | Removed legacy docs example notebook. |
| doc/examples/Inferring grids.ipynb | Removed legacy docs example notebook. |
| doc/examples/autonode_app.py | Removed legacy docs example script. |
| doc/examples.rst | Removed legacy Sphinx docs content. |
| doc/conf.py | Removed legacy Sphinx configuration. |
| doc/concepts/nodes.rst | Removed legacy Sphinx docs content. |
| doc/concepts/nodes.bak.rst | Removed legacy Sphinx docs content. |
| doc/concepts/index.rst | Removed legacy Sphinx docs content. |
| doc/concepts/data.rst | Removed legacy Sphinx docs content. |
| doc/api/plot.rst | Removed legacy Sphinx docs content. |
| doc/api/node.rst | Removed legacy Sphinx docs content. |
| doc/api/index.rst | Removed legacy Sphinx docs content. |
| doc/api/data.rst | Removed legacy Sphinx docs content. |
| .github/workflows/python-app.yml | Updated CI to install Qt libs via action + Xvfb setup for headless PySide6. |
| .github/actions/install-dependencies-and-plottr/action.yml | CI installs Plottr with .[pyside6] extra. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Remove the commented-out 'setup ubuntu-latest xvfb' step from the workflow
entirely (superseded by tlambert03/setup-qt-libs).
- Recreate the deleted test/gui/* interactive scripts as proper pytest-qt tests
under test/pytest (test_gui_widgets.py, test_gui_nodes.py,
test_gui_figuremaker.py): 22 qtbot-based smoke tests for widgets, node UIs and
both FigureMaker backends.
- Fix two latent bugs surfaced by those tests:
* MultiDimensionSelector used the Qt5-style unscoped enum
self.MultiSelection, which doesn't exist under PySide6; guard it the same
way DataSelectionWidget already does.
* PlotWithColorbar scatter coloring used colorbar.cmap, removed in pyqtgraph
0.14; use the public colorbar.colorMap() accessor instead.
- Make test_concurrent_write_and_read robust on Windows: wait for the writer
process to create the file before reading, instead of assuming it exists
after a fixed 2s sleep (Windows 'spawn' startup is slower than that).
- test_gui_nodes uses a fixture that forces node UI creation and restores
useUi/uiClass, so it is robust against other tests that set those to
False/None without restoring.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rewrite the README Installation section to explain that plottr uses qtpy and that exactly one Qt binding must be installed by the user, with a table of pip extras (pyside6/pyqt5/pyqt6/pyside2), QT_API selection guidance, and the conda note. Bump the documented Python requirement to 3.12 to match pyproject. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- monitr.VerticalScrollArea.on_range_changed: the rangeChanged(int, int) signal passes two ints; accept them in the slot signature/decorator (@slot(int, int)) instead of declaring @slot(int) on a no-arg method. - test_ddh5.test_loader_node: replace the fixed qtsleep(0.1) with qtbot.waitUntil(... output is not None), so the test waits for the queued onThreadComplete() slot to set the output rather than depending on timing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jenshnielsen
approved these changes
Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A faithful re-application of #439 ("Qt6 compatibility + some cleanup") onto the current
master. The original branch (pyside6_and_cleanup) is unmergeable —masterdiverged heavilyon
autoplot.py,inspectr.py,datadict.py,pyqtgraph/autoplot.py,monitr.py, etc. since theoriginal merge-base (
460fbbb). Rather than fight cherry-pick conflicts, the changes werere-implemented on top of current master so the intent is preserved while the result is clean and
passes checks.
Opened as draft for review of the differences and implementation. Original PR by @wpfff.
Validation
Run with uv (Python 3.12) using the repo's CI checks, with the PySide6 backend (PySide6 6.11.1):
mypy plottr→ Success: no issues found in 63 source filespytest test/pytest→ 411 passed (388 original + 22 new GUI tests + the previouslyWindows-flaky concurrent test, now fixed). The only console noise is pre-existing
libpyside: Failed to disconnect ... sigLevelsChangedRuntimeWarnings (already present on master).Buckets of change
1. Cleanup — file removals (clean, no conflicts)
doc/directory (old Sphinx docs; the plan in Qt6 compatibility + some cleanup #439 is to replace with mkdocs).readthedocs.yml(no longer hosting on RTD).test/gui/*,test/apps/*,test/prototyping/*,test/scripts/*,test/run_gui_test.py(manual/interactive prototyping scripts). The GUI scripts under
test/gui/were not just deleted —they were rewritten as real pytest-qt tests; see section 5.
2. Config / CI
pyproject.toml: addedpyside6 = ["PySide6>=6.0"]optional dependency; added a mypy overridedisabling
attr-definedforplottr.*(Qt enum access patterns differ between qtpy/PyQt5 andPySide6 stubs at type-check time, but work at runtime).
test_requirements.txt:PyQt5-stubs==5.15.6.0→PySide6-stubs.test/pytest/pytest.ini:qt_api=pyqt5→qt_api=pyside6..github/actions/install-dependencies-and-plottr/action.yml:pip install .[pyqt5]→.[pyside6]..github/workflows/python-app.yml: replaced the manualapt install ... xvfbstep withtlambert03/setup-qt-libs@v1+ a dedicated Xvfb "display" step (needed for headless PySide6).The old step was removed entirely (not left commented out) per review feedback.
3. Code — PySide6 compatibility
plottr/__init__.py(foundational): underTYPE_CHECKINGimport fromPySide6; exposeSignal/Slot,API_NAME, andPYSIDE6/PYQT6flags; exportQAction/QActionGroupfrom thecorrect module (they moved
QtWidgets→QtGuiin Qt6);loop.exec_()→loop.exec().apps/,plot/):*.exec_()→*.exec();QtWidgets.QAction(Group)→QAction/QActionGroup(imported fromplottr).appmanager.py: importPYSIDE6;QProcessstate check uses the Qt6 nested enum(
QProcess.ProcessState.NotRunning) vs Qt5 flat enum;qtsleep(0.01)→0.05; fixed theonProcessEnededtypo →onProcessEnded.inspectr.py:super().__init__(strings)→super().__init__(list(strings))(
QTreeWidgetItemneeds a real list under PySide6).json_viewer.py: import Qt symbols viaplottrand exposeAPI_NAME as __binding__(the old
__binding__reference was undefined);("PySide","PyQt4")→("PySide","PyQt5");Qt.ItemFlags→Qt.ItemFlag.monitr.py: QAction/QActionGroup migration; bug fixes —leaveEventcallingsuper().enterEvent→super().leaveEvent; null-guards inVerticalScrollArea.eventFilterandon_data_window_timer;on_adjust_column_width(None).data_display.py:setSelectionModeuses the Qt6 nested enum under PySide6.datadict.py:_DataAccess.__getattribute__now tolerates a missing_parent(try/except
AttributeError) to avoid pickling/unpickling errors.fitter.py:List[QtCore.pyqtBoundSignal]→List[Any](PyQt-specific type).# type: ignoreannotations: arg-type ignores on object/typed@Slots andoverrideignoreson Qt model/view method overrides, as required by mypy 2.1.0 + PySide6 stubs.
4. Test changes (existing suite)
test_app_manager.py: added the FIXME note ontest_getting_values(flaky on the author's Mac,fine on CI).
test_ddh5.py::test_loader_node: waits for the loader's queuedonThreadComplete()to set theflowchart output via
qtbot.waitUntil(...)(was a fixedqtsleep, see review fixes below).test_ddh5.py::test_concurrent_write_and_read(Windows robustness): the test spawns a writerProcessand used to read after a fixedtime.sleep(2). On Windows (spawn) the child needs~3.8 s just to import plottr/qcodes/Qt and create the file, so the first read raced against process
startup and failed (it passed on Linux
fork). It now waits for the file to be created beforereading — robust on both platforms, no lost coverage. Concurrent access itself is coordinated by
plottr's
FileOpenerlock file, so cross-platform concurrent read/write is fine once the file exists.5. New: rewrote the deleted
test/gui/scripts as pytest-qt testsThe old
test/gui/*files were interactive demo scripts (each ended inapp.exec_()), not part of themaintained suite. They are now proper
qtbot-based smoke tests that run in CI and verify the GUIbuilds and accepts data under PySide6. Added 22 tests across three files:
test/pytest/test_gui_widgets.py—DataSelectionWidget,DimensionSelector/AxisSelector/DependentSelector/MultiDimensionSelector,ShapeSpecificationWidget,GridOptionWidget,XYSelectionWidget.test/pytest/test_gui_nodes.py— node UIs in a flowchart (DataSelector,DDH5Loader,DimensionReducer,XYSelector,DataGridder) plus plot windows (SubtractAverage+PlotWindow, MPLAutoPlot).test/pytest/test_gui_figuremaker.py— MPL and pyqtgraphFigureMaker(line/image/scatter,including complex data).
Two latent bugs surfaced by these tests, and fixed:
gui/widgets.pyMultiDimensionSelectorused the Qt5-style unscoped enumself.MultiSelection,which doesn't exist under PySide6 — guarded it the same way
DataSelectionWidgetalready is.plot/pyqtgraph/plots.pyscatter coloring usedcolorbar.cmap, removed in pyqtgraph 0.14 — switchedto the public
colorbar.colorMap()accessor (still tracks interactive colormap changes).test_gui_nodes.pyuses a small fixture that forces node UI creation and restoresuseUi/uiClass,so it is robust against other suite modules that set those to
False/Nonewithout restoring.6. README
Rewrote the Installation section to explain that plottr talks to Qt through
qtpyand that exactlyone Qt binding must be installed by the user, with a table of pip extras
(
pyside6recommended /pyqt5/pyqt6/pyside2),QT_APIselection guidance for multi-bindingsetups, and the conda note. Bumped the documented Python requirement to 3.12 to match
pyproject.toml.Review feedback addressed
xvfbworkflow step entirely.monitr.VerticalScrollArea.on_range_changed:rangeChanged(int, int)passes two ints, so the slot isnow
@Slot(int, int)accepting(min_value, max_value)(was@Slot(int)on a no-arg method); themasking
# type: ignore[arg-type]was dropped and mypy stays green.test_loader_node: replaced the fixedqtsleep(0.1)withqtbot.waitUntil(... output is not None).Deliberate differences from #439
numpy.typing.mypy_pluginmypy line — already removed on master.mypy==2.1.0(not1.13.0) +hypothesis;actions/checkout@v7andactions/setup-python@v6.3.0.# type: ignoreset recomputed for mypy 2.1.0 + PySide6-stubs rather than copied verbatim fromQt6 compatibility + some cleanup #439 (which targeted mypy 1.13.0);
warn_unused_ignores=truemeans stale ignores fail.from build.lib.plottr import qtsleep; corrected (and theimport is now gone entirely after switching
test_loader_nodetoqtbot.waitUntil).plottr/apps/watchdog_classes.py(not touched by Qt6 compatibility + some cleanup #439): removed five now-redundant# type: ignore[attr-defined]comments that became "unused" onceattr-definedis globally disabledfor
plottr.*.Known / left items (for reviewer decision)
plottr/apps/ui/Monitr_UI.py(auto-generated, mypy-ignored, not imported anywhere) still usesQtWidgets.QAction, which doesn't exist at runtime under Qt6. Left untouched (matches Qt6 compatibility + some cleanup #439); theproper fix is to regenerate the
.ui. Flagged here since it's dead-but-latent under PySide6.leaveEvent→super().enterEventoccurrence exists nearfloating_button; Qt6 compatibility + some cleanup #439 only fixedthe
TextInput/save_buttonone, so that's all this PR changes.