From 12dd62ae17a12071e5ee05b4efa9881a75ceacbc Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:59:08 +0100 Subject: [PATCH 1/3] Fix SnapshotTool annotation test hanging on its snapshot dialog Since the fix of #58, SnapshotTool defers its action with a zero-delay timer. In test_annotation_tools, this action ran during the processEvents() call of guidata's exec_dialog, before the automatic closing of dialogs was scheduled, so the modal ResizeDialog opened by save_snapshot was never closed and the test hung. The test only passed under PyQt5 on Linux when run after another test, whose pending closing timer closed the dialog; it hung when run on its own, on Windows, and with PyQt6 or PySide6. In unattended mode, replace the snapshot action by a recorder, run the deferred actions within the test, and check that the action was called once with the plot and an oriented rectangle. Assisted-by: Claude Opus 5.5 --- doc/release_notes/release_2.12.md | 4 ++++ plotpy/tests/unit/test_annotation_tools.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/doc/release_notes/release_2.12.md b/doc/release_notes/release_2.12.md index e932697f..91316f6b 100644 --- a/doc/release_notes/release_2.12.md +++ b/doc/release_notes/release_2.12.md @@ -24,3 +24,7 @@ ⚙️ Dependencies: * Python 3.10 or later is now required, and wheels are no longer built for Python 3.9. Python 3.9 users must keep an earlier compatible release (PlotPy 2.11.x) (closes [Issue #74](https://github.com/PlotPyStack/PlotPy/issues/74)) + +🔧 Other changes: + +* **Test suite** — The `SnapshotTool` annotation test no longer hangs when run on its own, on Windows or with Qt 6: it waited indefinitely for the snapshot dialog to be closed diff --git a/plotpy/tests/unit/test_annotation_tools.py b/plotpy/tests/unit/test_annotation_tools.py index fb0c6679..a32f2f10 100644 --- a/plotpy/tests/unit/test_annotation_tools.py +++ b/plotpy/tests/unit/test_annotation_tools.py @@ -5,6 +5,7 @@ import numpy as np import pytest from guidata.qthelpers import exec_dialog, execenv, qt_app_context +from qtpy import QtWidgets as QW from plotpy.interfaces.items import IBasePlotItem, IShapeItemType from plotpy.tests import vistools as ptv @@ -97,13 +98,29 @@ def _test_annotation_tools(tool_classes: tuple[type[InteractiveTool], ...]): for tool_class in tool_classes: tool = win.manager.get_tool(tool_class) assert tool is not None + snapshot_calls: list[tuple] | None = None + if execenv.unattended and isinstance(tool, SnapshotTool): + # The snapshot action opens modal dialogs (destination size, options, + # file name) which are never closed in unattended mode + snapshot_calls = [] + tool.action_func = lambda *args, calls=snapshot_calls: calls.append( + args + ) tool.activate() x_path = np.linspace(0, 0.5, 100) y_path = np.linspace(0, 0.5, 100) with execenv.context(accept_dialogs=True): drag_mouse(win, x_path, y_path) + # Run the actions deferred by a zero-delay timer (e.g. the snapshot + # action) within this test rather than in a later event loop + QW.QApplication.processEvents() if hasattr(tool_class, "SWITCH_TO_DEFAULT_TOOL"): assert win.manager.get_default_tool() == default_tool + if snapshot_calls is not None: + assert len(snapshot_calls) == 1 + snap_plot, p0, p1 = snapshot_calls[0] + assert snap_plot is plot + assert p0.x() < p1.x() and p0.y() < p1.y() plot.unselect_all() shape_items: list[IBasePlotItem] = plot.get_items(item_type=IShapeItemType) plot.select_some_items(shape_items) From 57fbf80afd899ca2ed13c6449f357f467f54612a Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:05:01 +0100 Subject: [PATCH 2/3] Add PyQt6 and PySide6 CI workflows Add test_pyqt6.yml and test_pyside6.yml, based on test_pyqt5.yml as in guidata: Python 3.10 and 3.14, additional system libraries required by Qt 6, and a 20-minute timeout so that a hanging test does not hold a runner for hours. As in guidata and PythonQwt, PySide6 6.12.0 is excluded before Python 3.12, where it crashes. Pull requests now select the guidata and PythonQwt versions to test against from their target branch (github.base_ref), as pushes do from their own branch: pull requests to develop or release were tested against the versions published on PyPI, since github.ref_name is "/merge" for them. The PyQt6 tests are also required before uploading a release to PyPI, as in guidata. Closes #69 Assisted-by: Claude Opus 5.5 --- .github/workflows/build_deploy.yml | 5 +- .github/workflows/test_pyqt5.yml | 7 ++- .github/workflows/test_pyqt6.yml | 85 +++++++++++++++++++++++++++++ .github/workflows/test_pyside6.yml | 86 ++++++++++++++++++++++++++++++ doc/release_notes/release_2.12.md | 1 + 5 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test_pyqt6.yml create mode 100644 .github/workflows/test_pyside6.yml diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index f2a14f49..b2604e70 100644 --- a/.github/workflows/build_deploy.yml +++ b/.github/workflows/build_deploy.yml @@ -9,6 +9,9 @@ jobs: test: uses: ./.github/workflows/test_pyqt5.yml + test_pyqt6: + uses: ./.github/workflows/test_pyqt6.yml + build_wheels: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -57,7 +60,7 @@ jobs: path: dist/*.tar.gz upload_pypi: - needs: [test, build_wheels, build_sdist] + needs: [test, test_pyqt6, build_wheels, build_sdist] runs-on: ubuntu-latest environment: pypi permissions: diff --git a/.github/workflows/test_pyqt5.yml b/.github/workflows/test_pyqt5.yml index 8edb2bce..e5fe1469 100644 --- a/.github/workflows/test_pyqt5.yml +++ b/.github/workflows/test_pyqt5.yml @@ -31,6 +31,9 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies + env: + # Pull requests are routed by their target branch, pushes by their own branch + TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} run: | sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX @@ -38,7 +41,7 @@ jobs: python -m pip install ruff pytest pip install PyQt5 setuptools numpy Cython python setup.py build_ext --inplace - if [ "${{ github.ref_name }}" = "develop" ]; then + if [ "$TARGET_BRANCH" = "develop" ]; then pip uninstall -y guidata PythonQwt cd .. git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git @@ -53,7 +56,7 @@ jobs: pip install -r deps.txt # Install PlotPy without dependencies pip install --no-deps . - elif [ "${{ github.ref_name }}" = "release" ]; then + elif [ "$TARGET_BRANCH" = "release" ]; then pip uninstall -y guidata PythonQwt cd .. # Try cloning guidata from release, fallback to main or master diff --git a/.github/workflows/test_pyqt6.yml b/.github/workflows/test_pyqt6.yml new file mode 100644 index 00000000..a75650cb --- /dev/null +++ b/.github/workflows/test_pyqt6.yml @@ -0,0 +1,85 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions + +name: Build, install and test with PyQt6 + +on: + push: + branches: [ "master", "develop", "release" ] + pull_request: + branches: [ "master", "develop", "release" ] + workflow_call: + +jobs: + build: + + env: + DISPLAY: ':99.0' + + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.14"] + + steps: + - uses: actions/checkout@v7 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + env: + # Pull requests are routed by their target branch, pushes by their own branch + TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} + run: | + sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 libopengl0 libxcb-cursor0 + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + python -m pip install --upgrade pip + python -m pip install ruff pytest + pip install PyQt6 setuptools numpy Cython + python setup.py build_ext --inplace + if [ "$TARGET_BRANCH" = "develop" ]; then + pip uninstall -y guidata PythonQwt + cd .. + git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git + git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git + cd PlotPy + pip install -e ../guidata + pip install -e ../PythonQwt + # Install tomli for TOML parsing (safe if already present) + pip install tomli + # Extract dependencies and save to file, then install + python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata', 'PythonQwt'])]; open('deps.txt','w').write('\n'.join(deps))" + pip install -r deps.txt + # Install PlotPy without dependencies + pip install --no-deps . + elif [ "$TARGET_BRANCH" = "release" ]; then + pip uninstall -y guidata PythonQwt + cd .. + # Try cloning guidata from release, fallback to main or master + git clone --depth 1 --branch release https://github.com/PlotPyStack/guidata.git || git clone --depth 1 https://github.com/PlotPyStack/guidata.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/guidata.git + # Try cloning PythonQwt from main or master + git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/PythonQwt.git + cd PlotPy + pip install -e ../guidata + pip install -e ../PythonQwt + # Install tomli for TOML parsing (safe if already present) + pip install tomli + # Extract dependencies and save to file, then install + python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata', 'PythonQwt'])]; open('deps.txt','w').write('\n'.join(deps))" + pip install -r deps.txt + # Install PlotPy without dependencies + pip install --no-deps . + else + # Install from PyPI normally for master branch + pip install .[test] + fi + - name: Lint with Ruff + run: ruff check --output-format=github plotpy + - name: Test with pytest + run: | + pytest diff --git a/.github/workflows/test_pyside6.yml b/.github/workflows/test_pyside6.yml new file mode 100644 index 00000000..07f7e04f --- /dev/null +++ b/.github/workflows/test_pyside6.yml @@ -0,0 +1,86 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions + +name: Build, install and test with PySide6 + +on: + push: + branches: [ "master", "develop", "release" ] + pull_request: + branches: [ "master", "develop", "release" ] + workflow_call: + +jobs: + build: + + env: + DISPLAY: ':99.0' + + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.14"] + + steps: + - uses: actions/checkout@v7 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + env: + # Pull requests are routed by their target branch, pushes by their own branch + TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} + run: | + sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 libopengl0 libxcb-cursor0 + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + python -m pip install --upgrade pip + python -m pip install ruff pytest + # PySide6 6.12.0 drops a reference to None on each void call: fatal before Python 3.12 + pip install "PySide6!=6.12.0; python_version < '3.12'" "PySide6; python_version >= '3.12'" setuptools numpy Cython + python setup.py build_ext --inplace + if [ "$TARGET_BRANCH" = "develop" ]; then + pip uninstall -y guidata PythonQwt + cd .. + git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git + git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git + cd PlotPy + pip install -e ../guidata + pip install -e ../PythonQwt + # Install tomli for TOML parsing (safe if already present) + pip install tomli + # Extract dependencies and save to file, then install + python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata', 'PythonQwt'])]; open('deps.txt','w').write('\n'.join(deps))" + pip install -r deps.txt + # Install PlotPy without dependencies + pip install --no-deps . + elif [ "$TARGET_BRANCH" = "release" ]; then + pip uninstall -y guidata PythonQwt + cd .. + # Try cloning guidata from release, fallback to main or master + git clone --depth 1 --branch release https://github.com/PlotPyStack/guidata.git || git clone --depth 1 https://github.com/PlotPyStack/guidata.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/guidata.git + # Try cloning PythonQwt from main or master + git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/PythonQwt.git + cd PlotPy + pip install -e ../guidata + pip install -e ../PythonQwt + # Install tomli for TOML parsing (safe if already present) + pip install tomli + # Extract dependencies and save to file, then install + python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata', 'PythonQwt'])]; open('deps.txt','w').write('\n'.join(deps))" + pip install -r deps.txt + # Install PlotPy without dependencies + pip install --no-deps . + else + # Install from PyPI normally for master branch + pip install .[test] + fi + - name: Lint with Ruff + run: ruff check --output-format=github plotpy + - name: Test with pytest + run: | + pytest diff --git a/doc/release_notes/release_2.12.md b/doc/release_notes/release_2.12.md index 91316f6b..ece84d39 100644 --- a/doc/release_notes/release_2.12.md +++ b/doc/release_notes/release_2.12.md @@ -27,4 +27,5 @@ 🔧 Other changes: +* **Continuous integration** — PlotPy is now also tested with PyQt6 and PySide6 (Python 3.10 and 3.14) on each push and pull request, and with PyQt6 before each PyPI release. Pull requests targeting the `develop` or `release` branch are now tested against the development versions of guidata and PythonQwt, as pushes to these branches already were (closes [Issue #69](https://github.com/PlotPyStack/PlotPy/issues/69)) * **Test suite** — The `SnapshotTool` annotation test no longer hangs when run on its own, on Windows or with Qt 6: it waited indefinitely for the snapshot dialog to be closed From e1f4791e2be835ef58e4fc08d116fbd359be47dd Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:06:39 +0100 Subject: [PATCH 3/3] Test PyQt5 workflow on Python 3.10 and 3.14 only Test the lowest and highest supported Python versions, as the PyQt6 and PySide6 workflows do. Assisted-by: Claude Opus 5.5 --- .github/workflows/test_pyqt5.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pyqt5.yml b/.github/workflows/test_pyqt5.yml index e5fe1469..ecc0b1b1 100644 --- a/.github/workflows/test_pyqt5.yml +++ b/.github/workflows/test_pyqt5.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13", "3.14"] + python-version: ["3.10", "3.14"] steps: - uses: actions/checkout@v7