diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml index f2a14f4..b2604e7 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 8edb2bc..ecc0b1b 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 @@ -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 0000000..a75650c --- /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 0000000..07f7e04 --- /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 e932697..ece84d3 100644 --- a/doc/release_notes/release_2.12.md +++ b/doc/release_notes/release_2.12.md @@ -24,3 +24,8 @@ ⚙️ 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: + +* **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 diff --git a/plotpy/tests/unit/test_annotation_tools.py b/plotpy/tests/unit/test_annotation_tools.py index fb0c667..a32f2f1 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)