From 80e948d4935e5a2d1e424b06d71ecdb8bff84157 Mon Sep 17 00:00:00 2001 From: Andrey Fedorov Date: Thu, 2 Apr 2026 17:07:46 -0400 Subject: [PATCH 01/10] feat: auto-discover binaries from upstream release archive Introduce binaries.txt as the single source of truth for the list of dcmqi executables. CMakeLists.txt now reads it via file(STRINGS), __init__.py discovers installed binaries dynamically at import time, and tests parametrize from the file. The update-dcmqi workflow extracts binary names from the Linux archive and updates binaries.txt and pyproject.toml [project.scripts] automatically when a new release is detected. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/update-dcmqi.yml | 37 ++++++++++++++++- CMakeLists.txt | 16 ++------ binaries.txt | 6 +++ src/dcmqi/__init__.py | 66 +++++++++++++----------------- tests/test_executable.py | 24 +++-------- 5 files changed, 79 insertions(+), 70 deletions(-) create mode 100644 binaries.txt diff --git a/.github/workflows/update-dcmqi.yml b/.github/workflows/update-dcmqi.yml index f53bd80..cac0795 100644 --- a/.github/workflows/update-dcmqi.yml +++ b/.github/workflows/update-dcmqi.yml @@ -50,15 +50,32 @@ jobs: echo "Assets: $assets" # Download assets and compute checksums + # Also extract binary names from one archive (Linux) declare -A checksums + binaries="" for asset in $assets; do echo "Downloading $asset..." url="https://github.com/QIICR/dcmqi/releases/download/$latest_tag/$asset" - sha256=$(curl -sL "$url" | sha256sum | awk '{print $1}') + tmpfile=$(mktemp) + curl -sL "$url" -o "$tmpfile" + sha256=$(sha256sum "$tmpfile" | awk '{print $1}') checksums["$asset"]="$sha256" echo " SHA256: $sha256" + + # Discover binaries from the Linux archive + if [[ "$asset" == *-linux.tar.gz ]] && [ -z "$binaries" ]; then + echo "Discovering binaries from $asset..." + binaries=$(tar -tzf "$tmpfile" | grep '^[^/]*/bin/[^/]*$' | grep -v '/$' | sed 's|.*/bin/||' | sort) + echo " Discovered binaries: $(echo $binaries | tr '\n' ' ')" + fi + rm -f "$tmpfile" done + if [ -z "$binaries" ]; then + echo "ERROR: Could not discover binaries from Linux archive" + exit 1 + fi + # Determine macOS asset pattern has_mac_split=false for asset in $assets; do @@ -140,6 +157,22 @@ jobs: echo 'set(dcmqi_archive_url "https://github.com/QIICR/dcmqi/releases/download/v${version}/${dcmqi_archive_filename}")' } > dcmqiUrls.cmake + # Update binaries.txt with discovered binary list + echo "$binaries" > binaries.txt + + # Update pyproject.toml [project.scripts] to match binaries.txt + python3 - < Path: executable_path = f"dcmqi/bin/{name}" @@ -43,31 +30,34 @@ def _program(name: str, args: list[str]) -> int: return subprocess.call([_lookup(name), *args], close_fds=False) -def itkimage2segimage() -> NoReturn: - """Run the itkimage2segimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("itkimage2segimage", sys.argv[1:])) - - -def segimage2itkimage() -> NoReturn: - """Run the segimage2itkimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("segimage2itkimage", sys.argv[1:])) - +def _make_wrapper(name: str): + def _wrapper() -> NoReturn: + raise SystemExit(_program(name, sys.argv[1:])) -def tid1500writer() -> NoReturn: - """Run the tid1500writer executable with arguments passed to a Python script.""" - raise SystemExit(_program("tid1500writer", sys.argv[1:])) + _wrapper.__name__ = name + _wrapper.__qualname__ = name + _wrapper.__doc__ = f"Run the {name} executable with arguments passed to a Python script." + return _wrapper -def tid1500reader() -> NoReturn: - """Run the tid1500reader executable with arguments passed to a Python script.""" - raise SystemExit(_program("tid1500reader", sys.argv[1:])) - - -def itkimage2paramap() -> NoReturn: - """Run the itkimage2paramap executable with arguments passed to a Python script.""" - raise SystemExit(_program("itkimage2paramap", sys.argv[1:])) - - -def paramap2itkimage() -> NoReturn: - """Run the paramap2itkimage executable with arguments passed to a Python script.""" - raise SystemExit(_program("paramap2itkimage", sys.argv[1:])) +def _discover_binaries() -> list[str]: + """Return names of all executables installed in dcmqi/bin/.""" + files = distribution("dcmqi").files + if files is None: + return [] + binaries = [] + for _file in files: + parts = Path(str(_file)).parts + if len(parts) == 3 and parts[0] == "dcmqi" and parts[1] == "bin": + # Strip platform suffix (.exe on Windows) + name = Path(parts[2]).stem + binaries.append(name) + return sorted(binaries) + + +# Dynamically create wrapper functions for each installed binary +_binaries = _discover_binaries() +for _name in _binaries: + globals()[_name] = _make_wrapper(_name) + +__all__ = ["__version__", *_binaries] diff --git a/tests/test_executable.py b/tests/test_executable.py index 24ecf21..4a8157a 100644 --- a/tests/test_executable.py +++ b/tests/test_executable.py @@ -11,28 +11,16 @@ from . import push_argv -all_tools = pytest.mark.parametrize( - "tool", - [ - "itkimage2segimage", - "segimage2itkimage", - "tid1500writer", - "tid1500reader", - "itkimage2paramap", - "paramap2itkimage", - ], +_BINARIES_FILE = Path(__file__).parent.parent / "binaries.txt" +_EXPECTED_TOOLS = sorted( + line.strip() for line in _BINARIES_FILE.read_text().splitlines() if line.strip() ) +all_tools = pytest.mark.parametrize("tool", _EXPECTED_TOOLS) + all_tools_version = pytest.mark.parametrize( ("tool", "expected_version"), - [ - ("itkimage2segimage", "1.0"), - ("segimage2itkimage", "1.0"), - ("tid1500writer", "1.0"), - ("tid1500reader", "1.0"), - ("itkimage2paramap", "1.0"), - ("paramap2itkimage", "1.0"), - ], + [(t, "1.0") for t in _EXPECTED_TOOLS], ) From 9776eb0878e3f2e0b502f95657e58e528b9f3ea7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:08:25 +0000 Subject: [PATCH 02/10] style: pre-commit fixes --- src/dcmqi/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dcmqi/__init__.py b/src/dcmqi/__init__.py index 747729e..718a9dd 100644 --- a/src/dcmqi/__init__.py +++ b/src/dcmqi/__init__.py @@ -36,7 +36,9 @@ def _wrapper() -> NoReturn: _wrapper.__name__ = name _wrapper.__qualname__ = name - _wrapper.__doc__ = f"Run the {name} executable with arguments passed to a Python script." + _wrapper.__doc__ = ( + f"Run the {name} executable with arguments passed to a Python script." + ) return _wrapper From 2dae5d62f4d2ca2c7437d63dcbaee15543cd9093 Mon Sep 17 00:00:00 2001 From: Andrey Fedorov Date: Thu, 2 Apr 2026 17:14:14 -0400 Subject: [PATCH 03/10] style: fix pre-commit issues - Fix YAML-invalid heredoc in update-dcmqi.yml by collapsing Python script to a single-line python3 -c invocation - Add noqa: PLE0604 for dynamic __all__ in __init__.py - Add return type annotation to _make_wrapper - Apply ruff-format fix Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/update-dcmqi.yml | 12 +----------- src/dcmqi/__init__.py | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/update-dcmqi.yml b/.github/workflows/update-dcmqi.yml index cac0795..cc20520 100644 --- a/.github/workflows/update-dcmqi.yml +++ b/.github/workflows/update-dcmqi.yml @@ -161,17 +161,7 @@ jobs: echo "$binaries" > binaries.txt # Update pyproject.toml [project.scripts] to match binaries.txt - python3 - < int: return subprocess.call([_lookup(name), *args], close_fds=False) -def _make_wrapper(name: str): +def _make_wrapper(name: str) -> Callable[[], NoReturn]: def _wrapper() -> NoReturn: raise SystemExit(_program(name, sys.argv[1:])) @@ -62,4 +62,4 @@ def _discover_binaries() -> list[str]: for _name in _binaries: globals()[_name] = _make_wrapper(_name) -__all__ = ["__version__", *_binaries] +__all__ = ["__version__", *_binaries] # noqa: PLE0604 From 88f0b0043f635f49059bbd8d3449fc4ec6be5272 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:14:42 +0000 Subject: [PATCH 04/10] style: pre-commit fixes --- src/dcmqi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dcmqi/__init__.py b/src/dcmqi/__init__.py index 9f90a82..5135cf3 100644 --- a/src/dcmqi/__init__.py +++ b/src/dcmqi/__init__.py @@ -8,9 +8,10 @@ import subprocess import sys +from collections.abc import Callable from importlib.metadata import distribution from pathlib import Path -from typing import Callable, NoReturn +from typing import NoReturn from ._version import version as __version__ From 718d2aad02344f14d668599dc2fa23ac75537969 Mon Sep 17 00:00:00 2001 From: Andrey Fedorov Date: Thu, 2 Apr 2026 17:28:19 -0400 Subject: [PATCH 05/10] add missing binary to the list --- binaries.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/binaries.txt b/binaries.txt index 4b9727e..f6f327c 100644 --- a/binaries.txt +++ b/binaries.txt @@ -4,3 +4,4 @@ tid1500writer tid1500reader itkimage2paramap paramap2itkimage +bin2labelsegimage From b6b32acc2da3f10686d6ca8e65a0fdd71d7efc23 Mon Sep 17 00:00:00 2001 From: Andrey Fedorov Date: Thu, 2 Apr 2026 19:10:36 -0400 Subject: [PATCH 06/10] fix: add bin2labelsegimage to project.scripts entry points Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index f76bb83..62f40b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ tid1500writer = "dcmqi:tid1500writer" tid1500reader = "dcmqi:tid1500reader" itkimage2paramap = "dcmqi:itkimage2paramap" paramap2itkimage = "dcmqi:paramap2itkimage" +bin2labelsegimage = "dcmqi:bin2labelsegimage" [tool.scikit-build] minimum-version = "0.4" From 76afbbe9d226c3172029b5fc9c78c6f057e2f5ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 06:24:39 +0000 Subject: [PATCH 07/10] build(deps): Bump pypa/cibuildwheel in the actions group Bumps the actions group with 1 update: [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel). Updates `pypa/cibuildwheel` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/v3.4.0...v3.4.1) --- updated-dependencies: - dependency-name: pypa/cibuildwheel dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cd.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index f3b54d8..36e0116 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -107,7 +107,7 @@ jobs: with: fetch-depth: 0 - - uses: pypa/cibuildwheel@v3.4.0 + - uses: pypa/cibuildwheel@v3.4.1 env: CIBW_BUILD: "cp312-*" CIBW_ARCHS: "${{ matrix.arch }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a137a55..bd152d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: with: fetch-depth: 0 - - uses: pypa/cibuildwheel@v3.4.0 + - uses: pypa/cibuildwheel@v3.4.1 env: CIBW_BUILD: "${{ matrix.python-version }}-*" CIBW_ARCHS: "${{ matrix.arch }}" From ca066add9d57962fd60537d79f146cb305f14118 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:42:37 +0000 Subject: [PATCH 08/10] chore: update pre-commit hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.8 → v0.15.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.8...v0.15.9) - [github.com/pre-commit/mirrors-mypy: v1.19.1 → v1.20.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.19.1...v1.20.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13302d9..32a418f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: args: [--prose-wrap=always] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.8" + rev: "v0.15.9" hooks: - id: ruff args: ["--fix", "--show-fixes"] @@ -53,7 +53,7 @@ repos: types_or: [c++, c, cuda] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.19.1" + rev: "v1.20.0" hooks: - id: mypy files: src|tests From ae721e403ad6563d991acde2fa52214d98b05bb4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:10:13 +0000 Subject: [PATCH 09/10] chore: update pre-commit hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.9 → v0.15.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.9...v0.15.10) - [github.com/pre-commit/mirrors-clang-format: v22.1.2 → v22.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.2...v22.1.3) - [github.com/pre-commit/mirrors-mypy: v1.20.0 → v1.20.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.20.0...v1.20.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32a418f..44320e8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,20 +40,20 @@ repos: args: [--prose-wrap=always] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.9" + rev: "v0.15.10" hooks: - id: ruff args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v22.1.2" + rev: "v22.1.3" hooks: - id: clang-format types_or: [c++, c, cuda] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.20.0" + rev: "v1.20.1" hooks: - id: mypy files: src|tests From 76cc8126d737c7fb7581ecee72c62affd1ef00c1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:38:03 +0000 Subject: [PATCH 10/10] chore: update pre-commit hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.15.10 → v0.15.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.15.10...v0.15.12) - [github.com/pre-commit/mirrors-clang-format: v22.1.3 → v22.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v22.1.3...v22.1.4) - [github.com/pre-commit/mirrors-mypy: v1.20.1 → v1.20.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.20.1...v1.20.2) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44320e8..e734273 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,20 +40,20 @@ repos: args: [--prose-wrap=always] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.15.10" + rev: "v0.15.12" hooks: - id: ruff args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: "v22.1.3" + rev: "v22.1.4" hooks: - id: clang-format types_or: [c++, c, cuda] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.20.1" + rev: "v1.20.2" hooks: - id: mypy files: src|tests