From aa0fcdf1654c1ccf8c522059e83ecc8582f82e04 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 14:25:05 +0200 Subject: [PATCH 01/13] Consolidate project config into pyproject and modernize CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove legacy packaging and test config files (setup.cfg, requirements.txt, .isort.cfg, napari_cellseg3d/_tests/pytest.ini, and its conftest). Migrate and update metadata in pyproject.toml: bump supported Python classifiers to 3.10–3.12, add napari manifest entry-point, include napari.yaml in package data, simplify dynamic fields, adjust optional dependencies (add PyQt6, pyside6, move pydensecrf2 to crf extra) and streamline dev dependencies (remove black/isort as direct dev deps). Update tooling rules (ruff token change) and remove redundant tool configs. Update tox.ini to target py310/311/312 across linux/windows/macos, expose platform mappings for GH Actions, and switch to using extras for test/crf/pyqt6 while keeping usedevelop and running pytest as the test command. --- .isort.cfg | 8 ----- napari_cellseg3d/_tests/conftest.py | 18 ------------ napari_cellseg3d/_tests/pytest.ini | 2 -- pyproject.toml | 36 +++++++++-------------- requirements.txt | 24 --------------- setup.cfg | 45 ----------------------------- tox.ini | 31 +++++--------------- 7 files changed, 22 insertions(+), 142 deletions(-) delete mode 100644 .isort.cfg delete mode 100644 napari_cellseg3d/_tests/conftest.py delete mode 100644 napari_cellseg3d/_tests/pytest.ini delete mode 100644 requirements.txt delete mode 100644 setup.cfg diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 2b497c7c0..000000000 --- a/.isort.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[settings] -force_single_line = True -force_sort_within_sections = False -lexicographical = True -single_line_exclusions = ('typing',) -order_by_type = False -group_by_package = True -skip=__init__.py diff --git a/napari_cellseg3d/_tests/conftest.py b/napari_cellseg3d/_tests/conftest.py deleted file mode 100644 index bbfeff10e..000000000 --- a/napari_cellseg3d/_tests/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -import pytest - - -@pytest.fixture(scope="session", autouse=True) -def env_config(): - """ - Configure environment variables needed for the test session - """ - - # This makes QT render everything offscreen and thus prevents - # any Modals / Dialogs or other Widgets being rendered on the screen while running unit tests - os.environ["QT_QPA_PLATFORM"] = "offscreen" - - yield - - os.environ.pop("QT_QPA_PLATFORM") diff --git a/napari_cellseg3d/_tests/pytest.ini b/napari_cellseg3d/_tests/pytest.ini deleted file mode 100644 index 45c3be1c0..000000000 --- a/napari_cellseg3d/_tests/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -qt_api=pyqt5 diff --git a/pyproject.toml b/pyproject.toml index 329d119cc..0800f5556 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,9 @@ classifiers = [ "Topic :: Software Development :: Testing", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", "License :: OSI Approved :: MIT License", "Topic :: Scientific/Engineering :: Artificial Intelligence", @@ -43,17 +43,19 @@ dependencies = [ # "nibabel", # "pillow", "pyclesperanto", - "tqdm", "matplotlib", "pydensecrf2", ] -dynamic = ["version", "entry-points"] +dynamic = ["version"] [project.urls] Homepage = "https://github.com/AdaptiveMotorControlLab/CellSeg3D" Documentation = "https://adaptivemotorcontrollab.github.io/cellseg3d-docs/res/welcome.html" Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" +[project.entry-points."napari.manifest"] +"napari_cellseg3d" = "napari_cellseg3d:napari.yaml" + [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" @@ -65,7 +67,7 @@ include-package-data = true where = ["."] [tool.setuptools.package-data] -"*" = ["res/*.png", "code_models/models/pretrained/*.json", "*.yaml"] +"*" = ["res/*.png", "code_models/models/pretrained/*.json", "*.yaml", "napari.yaml"] [tool.ruff] select = [ @@ -79,7 +81,7 @@ select = [ "PTH", "RET", "SIM", - "TCH", + "TC", "NPY", ] # Never enforce `E501` (line length violations) and 'E741' (ambiguous variable names) @@ -116,23 +118,13 @@ exclude = [ [tool.ruff.pydocstyle] convention = "google" -[tool.black] -line-length = 79 - -[tool.isort] -profile = "black" -line_length = 79 - [project.optional-dependencies] -pyqt5 = [ - "pyqt5", -] -pyside2 = [ - "pyside2", -] pyside6 = [ "pyside6", ] +pyqt6 = [ + "PyQt6", +] onnx-cpu = [ "onnx", "onnxruntime" @@ -145,11 +137,8 @@ wandb = [ "wandb" ] dev = [ - "isort", - "black", "ruff", "pre-commit", - "tuna", "twine", ] docs = [ @@ -165,3 +154,6 @@ test = [ "onnx", "onnxruntime", ] +crf = [ + "pydensecrf2", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1411411de..000000000 --- a/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -black -coverage -imageio-ffmpeg>=0.4.5 -isort -itk -jupyter-book -pytest -pytest-qt -tox -twine -numpy -napari[all]>=0.4.14 -QtPy -opencv-python>=4.5.5 -pre-commit -pyclesperanto>=0.18.3 -matplotlib>=3.4.1 -ruff -tifffile>=2022.2.9 -torch>=1.11 -monai[nibabel,einops,tifffile]>=1.0.1 -pillow -scikit-image>=0.19.2 -vispy>=0.9.6 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 001bca5f7..000000000 --- a/setup.cfg +++ /dev/null @@ -1,45 +0,0 @@ -[metadata] -name = napari_cellseg3d -version = 0.2.2 - -[options] -packages = find: -include_package_data = True -python_requires = >=3.8 -package_dir = - =. - -# add your package requirements here -install_requires = - numpy - napari[all]>=0.4.14 - QtPy - opencv-python>=4.5.5 - scikit-image>=0.19.2 - matplotlib>=3.4.1 - tifffile>=2022.2.9 - imageio-ffmpeg>=0.4.5 - torch>=1.11 - monai[nibabel,einops,tifffile]>=1.0.1 - itk - tqdm - nibabel - pyclesperanto - scikit-image - pillow - tqdm - matplotlib - vispy>=0.9.6 - -[options.packages.find] -where = . - -[options.package_data] -napari_cellseg3d = - res/*.png - code_models/models/pretrained/*.json - napari.yaml - -[options.entry_points] -napari.manifest = - napari_cellseg3d = napari_cellseg3d:napari.yaml diff --git a/tox.ini b/tox.ini index a0c9ec277..da1b2b106 100644 --- a/tox.ini +++ b/tox.ini @@ -1,26 +1,25 @@ # For more information about tox, see https://tox.readthedocs.io/en/latest/ [tox] -envlist = py{38,39,310}-{linux} -; envlist = py{38,39,310}-{linux,macos,windows} +envlist = py{310,311,312}-{linux} isolated_build=true [gh-actions] python = - 3.8: py38 - 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 [gh-actions:env] PLATFORM = ubuntu-latest: linux - ; windows-latest: windows - ; macos-latest: macos + windows-latest: windows + macos-latest: macos [testenv] platform = linux: linux - ; windows: win32 - ; macos: darwin + windows: win32 + macos: darwin passenv = CI PYTHONPATH @@ -29,20 +28,6 @@ passenv = XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN -deps = - pytest # https://docs.pytest.org/en/latest/contents.html - pytest-cov # https://pytest-cov.readthedocs.io/en/latest/ - napari - PyQt5 - magicgui - pytest-qt - qtpy - git+https://github.com/lucasb-eyer/pydensecrf.git@master#egg=pydensecrf - onnx - onnxruntime - monai[tifffile] -; pyopencl[pocl] -; opencv-python -extras = crf +extras = test,crf,pyqt6 usedevelop = true commands = pytest -v --color=yes --cov=napari_cellseg3d --cov-report=xml From e0b7c98f5675923707469a8acdf423bfa719e3f2 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 14:30:50 +0200 Subject: [PATCH 02/13] Disable flaky test; use ndarray.reshape; fix viewer Skip a test that causes GitHub Actions to freeze by importing pytest and marking it skipped (needs to be fixed or removed). In worker_inference, replace np.reshape(...) with the ndarray.reshape(...) call and remove an obsolete commented reshape for clarity. In dev_scripts/correct_labels, initialize a napari.Viewer and add the image via viewer.add_image instead of calling napari.view_image(), ensuring the image layer is properly created. --- napari_cellseg3d/_tests/test_weight_download.py | 8 +++++++- napari_cellseg3d/code_models/worker_inference.py | 4 ++-- napari_cellseg3d/dev_scripts/correct_labels.py | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/napari_cellseg3d/_tests/test_weight_download.py b/napari_cellseg3d/_tests/test_weight_download.py index e13924364..c7c0c7eb7 100644 --- a/napari_cellseg3d/_tests/test_weight_download.py +++ b/napari_cellseg3d/_tests/test_weight_download.py @@ -1,10 +1,16 @@ +import os +import pytest + from napari_cellseg3d.code_models.workers_utils import ( PRETRAINED_WEIGHTS_DIR, WeightsDownloader, ) -# DISABLED, causes GitHub actions to freeze +@pytest.mark.skipif( + os.getenv("GITHUB_ACTIONS") == "true", + reason="This test causes GitHub Actions to freeze", +) def test_weight_download(): downloader = WeightsDownloader() downloader.download_weights("test", "test.pth") diff --git a/napari_cellseg3d/code_models/worker_inference.py b/napari_cellseg3d/code_models/worker_inference.py index 46ba77ebb..65bf0d240 100644 --- a/napari_cellseg3d/code_models/worker_inference.py +++ b/napari_cellseg3d/code_models/worker_inference.py @@ -1,4 +1,5 @@ """Contains the :py:class:`~InferenceWorker` class, which is a custom worker to run inference jobs in.""" + import platform import sys from pathlib import Path @@ -268,7 +269,6 @@ def load_layer(self): f" please check for extra channel/batch dimensions" ) volume = utils.correct_rotation(volume) - # volume = np.reshape(volume, newshape=(1, 1, *volume.shape)) dims_check = volume.shape @@ -280,7 +280,7 @@ def load_layer(self): if self.config.model_info.name != "WNet3D" else lambda x: x ) - volume = np.reshape(volume, newshape=(1, *volume.shape)) + volume = volume.reshape((1, *volume.shape)) if self.config.sliding_window_config.is_enabled(): load_transforms = Compose( [ diff --git a/napari_cellseg3d/dev_scripts/correct_labels.py b/napari_cellseg3d/dev_scripts/correct_labels.py index 572ca429f..7ec43ccf3 100644 --- a/napari_cellseg3d/dev_scripts/correct_labels.py +++ b/napari_cellseg3d/dev_scripts/correct_labels.py @@ -211,7 +211,8 @@ def relabel( np.isin(artefact, i_labels_to_add), 0, artefact ) if viewer is None: - viewer = napari.view_image(image) + viewer = napari.Viewer() + images = viewer.add_image(image, name="image") else: viewer = viewer viewer.add_image(image, name="image") From 533a0ce7ef572d952e2e86bf63a077fa681cbeee Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:20:07 +0200 Subject: [PATCH 03/13] Enhance GitHub Actions CI and deploy workflow (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enhance GitHub Actions CI and deploy workflow Modernize and expand the test-and-deploy workflow: bump action versions (checkout/setup-python), enable pip caching, and broaden the matrix to ubuntu, windows, and macos with Python 3.10–3.12. Add fail-fast:false and platform-specific steps (Linux Qt libs, Windows OpenGL with pwsh). Install tox centrally and run tests per-OS with appropriate Qt/PyQt and PyVista env vars; upload coverage with codecov v6. Adjust deploy job to trigger on semver-style tags (refs/tags/v*), update setup actions, split build and publish steps (build then twine upload), and simplify dependency installs. * Fix missing OSs in tox.ini --- .github/workflows/test_and_deploy.yml | 103 ++++++++++++++++---------- tox.ini | 2 +- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index cb581b79a..599b574c2 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -1,6 +1,3 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: test and deploy on: @@ -8,7 +5,7 @@ on: branches: - main tags: - - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + - "v*" pull_request: branches: - main @@ -18,76 +15,100 @@ jobs: test: name: ${{ matrix.platform }} py${{ matrix.python-version }} runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false matrix: -# platform: [ubuntu-latest, windows-latest] # , macos-latest - platform: [ubuntu-latest] - python-version: ['3.8', '3.9'] #issues with monai and 3.10; pausing for now. users should use python 3.9 + platform: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + cache: pip -# these libraries enable testing on Qt on linux - - uses: tlambert03/setup-qt-libs@v1 + - name: Install Qt libraries on Linux + if: runner.os == 'Linux' + uses: tlambert03/setup-qt-libs@v1 -# strategy borrowed from vispy for installing opengl libs on windows - name: Install Windows OpenGL if: runner.os == 'Windows' + shell: pwsh run: | git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git - powershell gl-ci-helpers/appveyor/install_opengl.ps1 - if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1} - -# note: if you need dependencies from conda, considering using -# setup-miniconda: https://github.com/conda-incubator/setup-miniconda -# and -# tox-conda: https://github.com/tox-dev/tox-conda - - name: Install dependencies + ./gl-ci-helpers/appveyor/install_opengl.ps1 + if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) { + exit 0 + } else { + exit 1 + } + + - name: Install tox run: | python -m pip install --upgrade pip - python -m pip install setuptools tox tox-gh-actions - python -m pip install tifffile - python -m pip install monai[nibabel,einops,tifffile] -# pip install git+https://github.com/lucasb-eyer/pydensecrf.git@master#egg=pydensecrf - -# this runs the platform-specific tests declared in tox.ini - - name: Test with tox - uses: GabrielBB/xvfb-action@v1 # aganders3/headless-gui@v1 + python -m pip install tox tox-gh-actions + + - name: Test with tox on Linux + if: runner.os == 'Linux' + uses: GabrielBB/xvfb-action@v1 with: run: python -m tox env: PLATFORM: ${{ matrix.platform }} + QT_API: pyqt6 + PYTEST_QT_API: pyqt6 + VISPY_USE_APP: pyqt6 + QT_OPENGL: software + PYVISTA_OFF_SCREEN: true - - name: Coverage - uses: codecov/codecov-action@v2 + - name: Test with tox on Windows/macOS + if: runner.os != 'Linux' + run: python -m tox + env: + PLATFORM: ${{ matrix.platform }} + QT_API: pyqt6 + PYTEST_QT_API: pyqt6 + VISPY_USE_APP: pyqt6 + QT_OPENGL: software + PYVISTA_OFF_SCREEN: true + + - name: Upload coverage + uses: codecov/codecov-action@v6 + with: + files: ./coverage.xml + fail_ci_if_error: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} deploy: -# this will run when you have tagged a commit, starting with "v*" -# and requires that you have put your twine API key in your -# github secrets (see readme for details) needs: [test] runs-on: ubuntu-latest - if: contains(github.ref, 'tags') + if: startsWith(github.ref, 'refs/tags/v') + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: "3.x" - - name: Install dependencies + + - name: Install build tools run: | python -m pip install --upgrade pip - pip install -U setuptools setuptools_scm wheel twine build - - name: Build and publish + python -m pip install build twine + + - name: Build package + run: | + python -m build + + - name: Publish to PyPI env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} run: | - git tag - python -m build . twine upload dist/* diff --git a/tox.ini b/tox.ini index da1b2b106..36307e3f5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # For more information about tox, see https://tox.readthedocs.io/en/latest/ [tox] -envlist = py{310,311,312}-{linux} +envlist = py{310,311,312}-{linux,windows,macos} isolated_build=true [gh-actions] From fc6615cf25a75b944c02366708d56111816c6f5c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:31:37 +0200 Subject: [PATCH 04/13] Add setuptools_scm versioning and CI improvements Enable setuptools_scm-based versioning and tighten CI/build checks. Updates include: - Use setuptools_scm: add setuptools-scm to build-system requires and configure version_file in pyproject.toml so package version is generated into napari_cellseg3d/_version.py. - Make package import version dynamically: __init__.py now prefers the generated _version and falls back to importlib.metadata.version. - Bump minimum Python to >=3.10 in pyproject. - CI/workflow changes: fetch full Git history (fetch-depth: 0), restrict Codecov upload to ubuntu-latest + Python 3.12, pin the build job to Python 3.12, and add a twine check step before publishing. - Minor dev script cleanup: remove unused assignment when adding image to napari viewer. These changes improve reproducible versioning, ensure the build uses an appropriate Python/tooling set, and add a safety check before publishing. --- .github/workflows/test_and_deploy.yml | 11 ++++++++++- napari_cellseg3d/__init__.py | 7 ++++++- napari_cellseg3d/dev_scripts/correct_labels.py | 8 +++++--- pyproject.toml | 14 ++++++++++---- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 599b574c2..13d867886 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -24,6 +24,8 @@ jobs: steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 @@ -77,6 +79,7 @@ jobs: PYVISTA_OFF_SCREEN: true - name: Upload coverage + if: matrix.platform == 'ubuntu-latest' && matrix.python-version == '3.12' uses: codecov/codecov-action@v6 with: files: ./coverage.xml @@ -91,11 +94,13 @@ jobs: steps: - uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.x" + python-version: "3.12" - name: Install build tools run: | @@ -106,6 +111,10 @@ jobs: run: | python -m build + - name: Check package + run: | + python -m twine check dist/* + - name: Publish to PyPI env: TWINE_USERNAME: __token__ diff --git a/napari_cellseg3d/__init__.py b/napari_cellseg3d/__init__.py index 7a1ba361b..c53f096d4 100644 --- a/napari_cellseg3d/__init__.py +++ b/napari_cellseg3d/__init__.py @@ -1,3 +1,8 @@ """napari-cellseg3d - napari plugin for cell segmentation.""" -__version__ = "0.2.2" +try: + from napari_cellseg3d._version import version as __version__ +except ImportError: + from importlib.metadata import version + + __version__ = version("napari_cellseg3d") diff --git a/napari_cellseg3d/dev_scripts/correct_labels.py b/napari_cellseg3d/dev_scripts/correct_labels.py index 7ec43ccf3..694c91024 100644 --- a/napari_cellseg3d/dev_scripts/correct_labels.py +++ b/napari_cellseg3d/dev_scripts/correct_labels.py @@ -1,3 +1,5 @@ +"""Tools to correct labels and add missing labels to the label image.""" + import threading import time import warnings @@ -89,7 +91,7 @@ def add_label(old_label, artefact, new_label_path, i_labels_to_add): returns = [] -def ask_labels(unique_artefact, test=False): +def _ask_labels(unique_artefact, test=False): global returns returns = [] if not test: @@ -204,7 +206,7 @@ def relabel( while loop: # visualize the artefact and ask the user which label to add to the label image t = threading.Thread( - target=partial(ask_labels, test=test), args=(unique_artefact,) + target=partial(_ask_labels, test=test), args=(unique_artefact,) ) t.start() artefact_copy = np.where( @@ -212,7 +214,7 @@ def relabel( ) if viewer is None: viewer = napari.Viewer() - images = viewer.add_image(image, name="image") + viewer.add_image(image, name="image") else: viewer = viewer viewer.add_image(image, name="image") diff --git a/pyproject.toml b/pyproject.toml index 0800f5556..39df3afc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Visualization", ] license = {text = "MIT"} -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = [ "numpy", "napari[all]>=0.4.14", @@ -57,12 +57,19 @@ Issues = "https://github.com/AdaptiveMotorControlLab/CellSeg3D/issues" "napari_cellseg3d" = "napari_cellseg3d:napari.yaml" [build-system] -requires = ["setuptools", "wheel"] +requires = [ + "setuptools>=64", + "setuptools-scm>=8", + "wheel", +] build-backend = "setuptools.build_meta" [tool.setuptools] include-package-data = true +[tool.setuptools_scm] +version_file = "napari_cellseg3d/_version.py" + [tool.setuptools.packages.find] where = ["."] @@ -81,7 +88,6 @@ select = [ "PTH", "RET", "SIM", - "TC", "NPY", ] # Never enforce `E501` (line length violations) and 'E741' (ambiguous variable names) @@ -156,4 +162,4 @@ test = [ ] crf = [ "pydensecrf2", -] \ No newline at end of file +] From 30bac0ef4384576af2c446779119f5caec4fb3b9 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:33:12 +0200 Subject: [PATCH 05/13] Skip weight download test on CI Use the generic CI environment variable when skipping the weight download test (os.getenv("CI") instead of "GITHUB_ACTIONS") and update the skip reason to mention CI. Also add a small formatting tweak (blank line after import) and adjust file ending. --- napari_cellseg3d/_tests/test_weight_download.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/napari_cellseg3d/_tests/test_weight_download.py b/napari_cellseg3d/_tests/test_weight_download.py index c7c0c7eb7..6f6a0c48b 100644 --- a/napari_cellseg3d/_tests/test_weight_download.py +++ b/napari_cellseg3d/_tests/test_weight_download.py @@ -1,4 +1,5 @@ import os + import pytest from napari_cellseg3d.code_models.workers_utils import ( @@ -8,8 +9,8 @@ @pytest.mark.skipif( - os.getenv("GITHUB_ACTIONS") == "true", - reason="This test causes GitHub Actions to freeze", + os.getenv("CI") == "true", + reason="This test causes CI to freeze", ) def test_weight_download(): downloader = WeightsDownloader() From 6638e7724a0c568ddaa0f01fbffd322d51157055 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:44:17 +0200 Subject: [PATCH 06/13] Add tests for plugin_base SingleImage/Folder Expand unit test coverage for napari_cellseg3d.code_plugins.plugin_base. Rename a test for updating default paths and add many new tests covering: results path creation/validation, _build not implemented, navigation buttons, dock widget removal (including LookupError handling), dataset path extraction, folder plugin default path updates, dataset loading (images, labels, unsupervised), dataset load warnings, file/folder dialog integrations (filetype and path updates), and visibility helpers. Also add necessary imports and use plugin_base for monkeypatching ui/utils/logger behaviors. --- napari_cellseg3d/_tests/test_base_plugin.py | 360 +++++++++++++++++++- 1 file changed, 358 insertions(+), 2 deletions(-) diff --git a/napari_cellseg3d/_tests/test_base_plugin.py b/napari_cellseg3d/_tests/test_base_plugin.py index ff0496577..c30d6f9a0 100644 --- a/napari_cellseg3d/_tests/test_base_plugin.py +++ b/napari_cellseg3d/_tests/test_base_plugin.py @@ -1,11 +1,17 @@ from pathlib import Path +from types import SimpleNamespace +import pytest +from qtpy.QtWidgets import QWidget + +from napari_cellseg3d.code_plugins import plugin_base from napari_cellseg3d.code_plugins.plugin_base import ( + BasePluginFolder, BasePluginSingleImage, ) -def test_base_single_image(make_napari_viewer_proxy): +def test_base_single_image_update_default_paths(make_napari_viewer_proxy): viewer = make_napari_viewer_proxy() plugin = BasePluginSingleImage(viewer) @@ -13,7 +19,357 @@ def test_base_single_image(make_napari_viewer_proxy): test_image = str(test_folder / "res/test.tif") assert plugin._check_results_path(str(test_folder)) + plugin.image_path = test_image assert plugin._default_path[0] != test_image + plugin._update_default_paths() - assert plugin._default_path[0] == test_image + + assert plugin._default_path == [test_image, None, None] + + +def test_check_results_path_creates_missing_folder( + make_napari_viewer_proxy, tmp_path +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + results_dir = tmp_path / "new" / "results" + + assert not results_dir.exists() + assert plugin._check_results_path(str(results_dir)) + assert results_dir.is_dir() + + +def test_check_results_path_empty_string_returns_false( + make_napari_viewer_proxy, +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + assert plugin._check_results_path("") is False + + +def test_check_results_path_rejects_non_string(make_napari_viewer_proxy): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + with pytest.raises(TypeError, match="Expected string"): + plugin._check_results_path(None) + + +def test_single_image_build_not_implemented(make_napari_viewer_proxy): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + with pytest.raises( + NotImplementedError, match="To be defined in child classes" + ): + plugin._build() + + +def test_make_navigation_buttons(make_napari_viewer_proxy): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + plugin.addTab(SimpleNamespace(), "A") + plugin.addTab(SimpleNamespace(), "B") + plugin.setCurrentIndex(1) + + prev_button = plugin._make_prev_button() + next_button = plugin._make_next_button() + + prev_button.click() + assert plugin.currentIndex() == 0 + + next_button.click() + assert plugin.currentIndex() == 1 + + +def test_remove_docked_widgets_success(make_napari_viewer_proxy): + viewer = make_napari_viewer_proxy() + plugin = BasePluginSingleImage(viewer) + + dock = viewer.window.add_dock_widget(plugin, name="temporary dock") + plugin.docked_widgets = [dock] + plugin.container_docked = True + + assert plugin.remove_docked_widgets() is True + assert plugin.docked_widgets == [] + assert plugin.container_docked is False + + +def test_remove_docked_widgets_handles_lookup_error( + make_napari_viewer_proxy, monkeypatch +): + viewer = make_napari_viewer_proxy() + plugin = BasePluginSingleImage(viewer) + + plugin.docked_widgets = [object()] + plugin.container_docked = True + + def raise_lookup_error(_dock_widget): + raise LookupError + + monkeypatch.setattr( + viewer.window, "remove_dock_widget", raise_lookup_error + ) + + assert plugin.remove_docked_widgets() is False + + +def test_extract_dataset_paths_empty(): + assert BasePluginFolder.extract_dataset_paths([]) is None + + +def test_extract_dataset_paths_none(): + assert BasePluginFolder.extract_dataset_paths([None]) is None + + +def test_extract_dataset_paths_returns_parent(tmp_path): + image_path = tmp_path / "images" / "image.tif" + image_path.parent.mkdir() + image_path.write_text("fake") + + assert BasePluginFolder.extract_dataset_paths([str(image_path)]) == str( + image_path.parent + ) + + +def test_folder_update_default_paths_from_existing_paths( + make_napari_viewer_proxy, tmp_path +): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + image_dir = tmp_path / "images" + label_dir = tmp_path / "labels" + val_dir = tmp_path / "validation" + results_dir = tmp_path / "results" + + for folder in [image_dir, label_dir, val_dir, results_dir]: + folder.mkdir() + + plugin.images_filepaths = [str(image_dir / "img.tif")] + plugin.labels_filepaths = [str(label_dir / "lab.tif")] + plugin.validation_filepaths = [str(val_dir / "val.tif")] + plugin.results_path = str(results_dir) + + plugin._update_default_paths() + + assert plugin._default_path == [ + str(image_dir), + str(label_dir), + str(val_dir), + str(results_dir), + ] + + +def test_folder_update_default_paths_appends_existing_dir( + make_napari_viewer_proxy, + tmp_path, +): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + plugin._update_default_paths(str(tmp_path)) + + assert str(tmp_path) in plugin._default_path + + +def test_load_dataset_paths(make_napari_viewer_proxy, monkeypatch, tmp_path): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + image_0 = tmp_path / "0.tif" + image_1 = tmp_path / "1.tif" + image_0.write_text("fake") + image_1.write_text("fake") + + expected = [image_0, image_1] + + monkeypatch.setattr( + plugin_base.ui, + "open_folder_dialog", + lambda *_args, **_kwargs: str(tmp_path), + ) + monkeypatch.setattr( + plugin_base.utils, + "get_all_matching_files", + lambda _directory: expected, + ) + + assert plugin.load_dataset_paths() == expected + + +def test_load_dataset_paths_warns_when_empty( + make_napari_viewer_proxy, + monkeypatch, + tmp_path, +): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + warnings = [] + + monkeypatch.setattr( + plugin_base.ui, + "open_folder_dialog", + lambda *_args, **_kwargs: str(tmp_path), + ) + monkeypatch.setattr( + plugin_base.utils, + "get_all_matching_files", + lambda _directory: [], + ) + monkeypatch.setattr( + plugin_base.logger, + "warning", + lambda msg: warnings.append(msg), + ) + + assert plugin.load_dataset_paths() == [] + assert warnings + assert "does not contain any compatible" in warnings[0] + + +def test_load_image_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + image_0 = tmp_path / "b.tif" + image_1 = tmp_path / "a.tif" + image_0.write_text("fake") + image_1.write_text("fake") + + monkeypatch.setattr( + plugin, "load_dataset_paths", lambda: [image_0, image_1] + ) + + plugin.load_image_dataset() + + assert plugin.images_filepaths == [str(image_1), str(image_0)] + assert plugin.image_filewidget.text_field.text() == str(tmp_path) + + +def test_load_label_dataset(make_napari_viewer_proxy, monkeypatch, tmp_path): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + label_0 = tmp_path / "b.tif" + label_1 = tmp_path / "a.tif" + label_0.write_text("fake") + label_1.write_text("fake") + + monkeypatch.setattr( + plugin, "load_dataset_paths", lambda: [label_0, label_1] + ) + + plugin.load_label_dataset() + + assert plugin.labels_filepaths == [str(label_1), str(label_0)] + assert plugin.labels_filewidget.text_field.text() == str(tmp_path) + + +def test_load_unsup_images_dataset( + make_napari_viewer_proxy, monkeypatch, tmp_path +): + plugin = BasePluginFolder(make_napari_viewer_proxy()) + + image_0 = tmp_path / "b.tif" + image_1 = tmp_path / "a.tif" + image_0.write_text("fake") + image_1.write_text("fake") + + monkeypatch.setattr( + plugin, "load_dataset_paths", lambda: [image_0, image_1] + ) + + plugin.load_unsup_images_dataset() + + assert plugin.validation_filepaths == [str(image_1), str(image_0)] + assert plugin.unsupervised_images_filewidget.text_field.text() == str( + tmp_path + ) + + +def test_show_file_dialog_updates_filetype( + make_napari_viewer_proxy, + monkeypatch, + tmp_path, +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + image_path = tmp_path / "image.tif" + image_path.write_text("fake") + + monkeypatch.setattr( + plugin_base.ui, + "open_file_dialog", + lambda *_args, **_kwargs: [str(image_path)], + ) + + result = plugin._show_file_dialog() + + assert result == str(image_path) + assert plugin.filetype == ".tif" + + +def test_show_dialog_images_sets_image_path( + make_napari_viewer_proxy, + monkeypatch, + tmp_path, +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + image_path = tmp_path / "image.tif" + image_path.write_text("fake") + + monkeypatch.setattr(plugin, "_show_file_dialog", lambda: str(image_path)) + + plugin._show_dialog_images() + + assert plugin.image_path == str(image_path) + assert plugin.image_filewidget.text_field.text() == str(image_path) + + +def test_show_dialog_labels_sets_label_path( + make_napari_viewer_proxy, + monkeypatch, + tmp_path, +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + label_path = tmp_path / "label.tif" + label_path.write_text("fake") + + monkeypatch.setattr(plugin, "_show_file_dialog", lambda: str(label_path)) + + plugin._show_dialog_labels() + + assert plugin.label_path == str(label_path) + assert plugin.labels_filewidget.text_field.text() == str(label_path) + + +def test_load_results_path_sets_results_path( + make_napari_viewer_proxy, + monkeypatch, + tmp_path, +): + plugin = BasePluginSingleImage(make_napari_viewer_proxy()) + + monkeypatch.setattr( + plugin_base.ui, + "open_folder_dialog", + lambda *_args, **_kwargs: str(tmp_path), + ) + + plugin._load_results_path() + + assert plugin.results_path == str(tmp_path.resolve()) + assert plugin.results_filewidget.text_field.text() == str( + tmp_path.resolve() + ) + + +def test_show_and_hide_io_element_without_toggle(qtbot): + widget = QWidget() + widget.setVisible(False) + qtbot.addWidget(widget) + + BasePluginSingleImage._show_io_element(widget) + + assert widget.isVisible() + + BasePluginSingleImage._hide_io_element(widget) + + assert not widget.isVisible() From 050da21401ea64e2afda5c877811fbf44a1e00c1 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:45:21 +0200 Subject: [PATCH 07/13] Set Windows env vars in tox.ini Add Windows-specific environment variables to tox.ini passenv: set USERNAME to 'runneradmin' and TORCHINDUCTOR_CACHE_DIR to {envtmpdir}/torchinductor. Ensures Windows test runs use a consistent username and place TorchInductor cache in the temporary directory. --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index 36307e3f5..dcf922f5a 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,10 @@ passenv = XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN + + windows: USERNAME = runneradmin + windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor + extras = test,crf,pyqt6 usedevelop = true commands = pytest -v --color=yes --cov=napari_cellseg3d --cov-report=xml From 7be7755ba8df22267603ab11f26242da619f1449 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:47:16 +0200 Subject: [PATCH 08/13] Use qtbot and QWidget in navigation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update test_make_navigation_buttons to use real QWidget instances and the qtbot fixture instead of SimpleNamespace. The test now accepts qtbot, creates two QWidgets, registers them with qtbot.addWidget, and adds them as tabs before checking navigation buttons—ensuring proper Qt widget management and avoiding teardown/warning issues. --- napari_cellseg3d/_tests/test_base_plugin.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/napari_cellseg3d/_tests/test_base_plugin.py b/napari_cellseg3d/_tests/test_base_plugin.py index c30d6f9a0..188a91357 100644 --- a/napari_cellseg3d/_tests/test_base_plugin.py +++ b/napari_cellseg3d/_tests/test_base_plugin.py @@ -64,11 +64,14 @@ def test_single_image_build_not_implemented(make_napari_viewer_proxy): plugin._build() -def test_make_navigation_buttons(make_napari_viewer_proxy): +def test_make_navigation_buttons(make_napari_viewer_proxy, qtbot): plugin = BasePluginSingleImage(make_napari_viewer_proxy()) - - plugin.addTab(SimpleNamespace(), "A") - plugin.addTab(SimpleNamespace(), "B") + a_wdg = QWidget() + b_wdg = QWidget() + qtbot.addWidget(a_wdg) + qtbot.addWidget(b_wdg) + plugin.addTab(a_wdg, "A") + plugin.addTab(b_wdg, "B") plugin.setCurrentIndex(1) prev_button = plugin._make_prev_button() From 928902026ee475036cf8fe4732923cae1e2d31a3 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 15:51:03 +0200 Subject: [PATCH 09/13] Add setenv header to tox.ini Insert the missing `setenv` key in tox.ini so the Windows-specific environment variables (USERNAME, TORCHINDUCTOR_CACHE_DIR) are declared under the correct section. This fixes configuration formatting and ensures those variables are applied in tox runs on Windows. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dcf922f5a..6f2960ca4 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ passenv = XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN - +setenv = windows: USERNAME = runneradmin windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor From 42966677248055cec170488b0ffdf2c6f35b22ec Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 16:00:01 +0200 Subject: [PATCH 10/13] Fix optional PyQt deps Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 39df3afc2..d2f795652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,10 +126,10 @@ convention = "google" [project.optional-dependencies] pyside6 = [ - "pyside6", + "napari[pyside6]", ] pyqt6 = [ - "PyQt6", + "napari[pyqt6]", ] onnx-cpu = [ "onnx", From 89921c70a08ebe9d7eaac34540c4b3ac490e3ceb Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 16:02:24 +0200 Subject: [PATCH 11/13] Update tox.ini --- tox.ini | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tox.ini b/tox.ini index 6f2960ca4..90bd5d9dd 100644 --- a/tox.ini +++ b/tox.ini @@ -20,17 +20,24 @@ platform = linux: linux windows: win32 macos: darwin -passenv = + +set_env = + QT_API = pyqt6 + PYTEST_QT_API = pyqt6 + VISPY_USE_APP = pyqt6 + QT_OPENGL = software + PYVISTA_OFF_SCREEN = true + + windows: USERNAME = runneradmin + windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor + +pass_env = CI - PYTHONPATH GITHUB_ACTIONS + PYTHONPATH DISPLAY XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION - PYVISTA_OFF_SCREEN -setenv = - windows: USERNAME = runneradmin - windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor extras = test,crf,pyqt6 usedevelop = true From 63915b5904bb08def334a71233bdba2703dc8782 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 16:16:03 +0200 Subject: [PATCH 12/13] Revert "Update tox.ini" This reverts commit 89921c70a08ebe9d7eaac34540c4b3ac490e3ceb. --- tox.ini | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index 90bd5d9dd..6f2960ca4 100644 --- a/tox.ini +++ b/tox.ini @@ -20,24 +20,17 @@ platform = linux: linux windows: win32 macos: darwin - -set_env = - QT_API = pyqt6 - PYTEST_QT_API = pyqt6 - VISPY_USE_APP = pyqt6 - QT_OPENGL = software - PYVISTA_OFF_SCREEN = true - - windows: USERNAME = runneradmin - windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor - -pass_env = +passenv = CI - GITHUB_ACTIONS PYTHONPATH + GITHUB_ACTIONS DISPLAY XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +setenv = + windows: USERNAME = runneradmin + windows: TORCHINDUCTOR_CACHE_DIR = {envtmpdir}/torchinductor extras = test,crf,pyqt6 usedevelop = true From c675b7aca732612131bb4884f4daeb8fcad2daa9 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 19 May 2026 16:29:48 +0200 Subject: [PATCH 13/13] Use napari viewer fixture in test_relabel Update test_relabel to use the make_napari_viewer_proxy fixture: create a viewer instance and pass it to cl.relabel via viewer=viewer. This ensures the relabel test runs with a proxied Napari viewer (e.g. for headless/test environments) and verifies behavior when a viewer is provided. --- napari_cellseg3d/_tests/test_labels_correction.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/napari_cellseg3d/_tests/test_labels_correction.py b/napari_cellseg3d/_tests/test_labels_correction.py index b4f13238c..bd1b0412c 100644 --- a/napari_cellseg3d/_tests/test_labels_correction.py +++ b/napari_cellseg3d/_tests/test_labels_correction.py @@ -37,12 +37,14 @@ def test_correct_labels(): ) -def test_relabel(): +def test_relabel(make_napari_viewer_proxy): + viewer = make_napari_viewer_proxy() cl.relabel( str(image_path), str(labels_path), go_fast=True, test=True, + viewer=viewer, )