From 0363f132cfb23216d0abe6f3381656aecba927ce Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 24 Nov 2025 14:14:25 +0100 Subject: [PATCH 01/22] WIP setup mkdocs --- .github/workflows/build.yaml | 104 ++++++----- .gitignore | 5 +- docs/changelog.md | 5 + docs/compatibility.md | 7 + docs/index.md | 1 + mkdocs.yaml | 165 ++++++++++++++++++ pyproject.toml | 14 +- scripts/generate_api_doc_pages.py | 37 ++++ scripts/pdoc/create_pydantic_patch.sh | 25 --- .../pdoc/mark_pydantic_attrs_private.patch | 28 --- scripts/pdoc/run.sh | 16 -- src/bioimageio/core/cli.py | 39 +++-- src/bioimageio/core/tensor.py | 4 +- .../core/weight_converters/_add_weights.py | 2 +- 14 files changed, 312 insertions(+), 140 deletions(-) create mode 100644 docs/changelog.md create mode 100644 docs/compatibility.md create mode 100644 docs/index.md create mode 100644 mkdocs.yaml create mode 100644 scripts/generate_api_doc_pages.py delete mode 100755 scripts/pdoc/create_pydantic_patch.sh delete mode 100644 scripts/pdoc/mark_pydantic_attrs_private.patch delete mode 100755 scripts/pdoc/run.sh diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5874d7e4..1e67773a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: force-publish: - description: 'Force publish even if no version change detected' + description: 'Force publish even if no version change was detected' required: false type: choice options: @@ -190,34 +190,6 @@ jobs: env: BIOIMAGEIO_CACHE_PATH: bioimageio_cache - docs: - needs: [coverage, test] - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: coverage-summary - path: dist - - uses: actions/setup-python@v6 - with: - python-version: '3.12' - cache: 'pip' - - run: pip install -e .[dev,partners] - - name: Generate developer docs - run: ./scripts/pdoc/run.sh - - run: cp README.md ./dist/README.md - - name: copy rendered presentations - run: | - mkdir ./dist/presentations - cp -r ./presentations/*.html ./dist/presentations/ - - name: Deploy to gh-pages 🚀 - uses: JamesIves/github-pages-deploy-action@v4 - with: - branch: gh-pages - folder: dist - build: runs-on: ubuntu-latest steps: @@ -235,21 +207,33 @@ jobs: path: dist/ name: dist - publish: - needs: [test, build, conda-build, docs] + docs: + needs: [build, conda-build, coverage, test] runs-on: ubuntu-latest - environment: - name: release - url: https://pypi.org/project/bioimageio.core/ permissions: contents: write # required for tag creation - id-token: write # required for pypi publish action + outputs: + new-version: ${{ steps.get-new-version.outputs.new-version }} steps: - - name: Check out the repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 fetch-tags: true + - uses: actions/download-artifact@v4 + with: + name: coverage-summary + path: dist + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'pip' + - run: pip install -e .[dev,docs,partners] + - name: Get branch name to deploy to + id: get_branch + shell: bash + run: | + if [[ -n '${{ github.event.pull_request.head.ref }}' ]]; then branch=gh-pages-${{ github.event.pull_request.head.ref }}; else branch=gh-pages; fi + echo "::set-output name=branch::$branch" - name: Get parent commit if: inputs.force-publish != 'true' id: get-parent-commit @@ -258,7 +242,6 @@ jobs: - id: get-existing-tag if: inputs.force-publish == 'true' run: echo "existing-tag=$(git tag --points-at HEAD 'v[0-9]*.[0-9]*.[0-9]*')" >> $GITHUB_OUTPUT - - name: Detect new version from last commit and create tag id: tag-version if: github.ref == 'refs/heads/main' && steps.get-parent-commit.outputs.sha && inputs.force-publish != 'true' @@ -273,8 +256,6 @@ jobs: import os from pathlib import Path - - if "${{ inputs.force-publish }}" == "true": existing_tag = "${{ steps.get-existing-tag.outputs.existing-tag }}" valid = existing_tag.count("v") == 1 and existing_tag.count(".") == 2 and all(part.isdigit() for part in existing_tag.lstrip("v").split(".")) @@ -291,23 +272,52 @@ jobs: with open(os.environ['GITHUB_OUTPUT'], 'a') as f: print(f"new-version={new_version}", file=f) + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - name: Generate developer docs + run: mike deploy --push --branch ${{ steps.get_branch.outputs.branch }} --update-aliases ${{ steps.get-new-version.outputs.new-version || 'dev'}} ${{ steps.get-new-version.outputs.new-version && 'latest' || ' '}} + - name: copy rendered presentations + run: | + mkdir ./dist/presentations + cp -r ./presentations/*.html ./dist/presentations/ + - name: Deploy to gh-pages 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: dist + clean: true + clean-exclude: | + .nojekyll + index.html + versions.json + latest/ + dev/ + v0.*/ + publish: + needs: [test, coverage, build, conda-build, docs] + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' && needs.docs.outputs.new-version + environment: + name: release + url: https://pypi.org/project/bioimageio.core/ + permissions: + contents: write # required to create a github release (release drafter) + id-token: write # required for pypi publish action + steps: - uses: actions/download-artifact@v4 - if: github.ref == 'refs/heads/main' && steps.get-new-version.outputs.new-version with: name: dist path: dist - name: Publish package on PyPI - if: github.ref == 'refs/heads/main' && steps.get-new-version.outputs.new-version uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ - - name: Publish the release notes - if: github.ref == 'refs/heads/main' uses: release-drafter/release-drafter@v6.0.0 with: - publish: "${{ steps.get-new-version.outputs.new-version != '' }}" - tag: '${{ steps.get-new-version.outputs.new-version }}' + tag: '${{ needs.docs.outputs.new-version }}' env: GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.gitignore b/.gitignore index 688e4a88..3cb4fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,12 +6,13 @@ __pycache__/ *.egg-info/ *.pyc **/tmp +bioimageio_cache/ bioimageio_unzipped_tf_weights/ build/ cache coverage.xml dist/ -docs/ dogfood/ +pkgs/ +site/ typings/pooch/ -bioimageio_cache/ diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..a38b0a23 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ +--- +title: Changelog +--- + +--8<-- "changelog.md" diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 00000000..5143266e --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,7 @@ +# Compatibility with bioimage.io resources + +bioimageio.core is used on [bioimage.io](https://bioimage.io) to test resources during and after the upload process. +Results are reported as "Test reports" (bioimageio.core deployed in a generic Python environment) +as well as the bioimageio.core tool compatibility (testing a resource with bioimageio.core in a dedicated Python environment). + +An overview of the latter is available [as part of the collection documentation](https://bioimage-io.github.io/collection/latest/reports_overview/). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..612c7a5e --- /dev/null +++ b/docs/index.md @@ -0,0 +1 @@ +--8<-- "README.md" diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 00000000..fb062233 --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,165 @@ +site_name: 'bioimageio.core' +site_url: 'https://bioimage-io.github.io/core-bioimage-io-python' +site_author: Fynn Beuttenmüller +site_description: 'Python specific core utilities for bioimage.io resources (in particular DL models).' + +repo_name: bioimage-io/core-bioimage-io-python +repo_url: https://github.com/bioimage-io/core-bioimage-io-python +edit_uri: edit/main/docs/ + +theme: + name: material + features: + - announce.dismiss + - content.action.edit + - content.action.view + - content.code.annotate + - content.code.copy + - content.code.select + - content.footnote.tooltips + - content.tabs.link + - content.tooltips + - header.autohide + - navigation.expand + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.preview + - navigation.instant.progress + - navigation.path + - navigation.prune + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.tracking + - search.highlight + - search.share + - search.suggest + - toc.follow + # - toc.integrate + + palette: + - media: '(prefers-color-scheme)' + primary: 'deep-purple' + accent: 'blue' + toggle: + icon: material/brightness-auto + name: 'Switch to light mode' + - media: '(prefers-color-scheme: light)' + scheme: default + primary: 'deep-purple' + accent: 'blue' + toggle: + icon: material/brightness-7 + name: 'Switch to dark mode' + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: 'deep-purple' + accent: 'blue' + toggle: + icon: material/brightness-4 + name: 'Switch to system preference' + + font: + text: Roboto + code: Roboto Mono + + logo: images/bioimage-io-icon.png + favicon: images/favicon.ico + +plugins: + - autorefs + - coverage: + html_report_dir: dist/coverage + - markdown-exec + - mkdocstrings: + handlers: + python: + inventories: + - https://docs.pydantic.dev/latest/objects.inv + - https://bioimage-io.github.io/spec-bioimage-io/latest/objects.inv + - https://bioimage-io.github.io/spec-bioimage-io/dev/objects.inv + options: + annotations_path: source + backlinks: tree + docstring_options: + ignore_init_summary: true + docstring_section_style: spacy + filters: 'public' + heading_level: 1 + inherited_members: true + members: true + merge_init_into_class: true + parameter_headings: true + separate_signature: true + scoped_crossrefs: true + show_root_heading: true + show_root_full_path: false + show_signature_annotations: true + show_if_no_docstring: true + show_source: true + show_symbol_type_heading: true + show_symbol_type_toc: true + # unwrap_annotated: true + signature_crossrefs: true + summary: true + extensions: + - griffe_pydantic: + schema: true + show_inheritance_diagram: true + - mike: + alias_type: symlink + canonical_version: latest + version_selector: true + - gen-files: + scripts: + - scripts/generate_api_doc_pages.py + - literate-nav: + nav_file: SUMMARY.md + - search + - section-index + +markdown_extensions: + - attr_list + - admonition + - callouts: + strip_period: false + - footnotes + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + pygments_lang_class: true + - pymdownx.magiclink + - pymdownx.snippets: + base_path: [!relative $config_dir] + check_paths: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + slugify: !!python/object/apply:pymdownx.slugs.slugify + kwds: + case: lower + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + - toc: + permalink: '¤' + +nav: + - Home: + - index.md + - Compatibility: compatibility.md + - API Reference: reference/ + - Changelog: changelog.md + - Coverage report: coverage.md + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/bioimage-io + version: + provider: mike diff --git a/pyproject.toml b/pyproject.toml index ed7e9aa3..6157b87e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,6 @@ dev = [ "onnxruntime", "onnxscript", "packaging>=17.0", - "pdoc", "pre-commit", "pyright==1.1.407", "pytest-cov", @@ -73,6 +72,19 @@ dev = [ "torch>=1.6,<3", "torchvision>=0.21", ] +docs = [ + "griffe-pydantic", + "markdown-callouts", + "markdown-exec", + "mike", + "mkdocs-api-autonav", + "mkdocs-coverage", + "mkdocs-gen-files", + "mkdocs-literate-nav", + "mkdocs-literate-nav", + "mkdocs-material", + "mkdocs-section-index", +] [build-system] requires = ["pip", "setuptools>=61.0"] diff --git a/scripts/generate_api_doc_pages.py b/scripts/generate_api_doc_pages.py new file mode 100644 index 00000000..a4ab1a82 --- /dev/null +++ b/scripts/generate_api_doc_pages.py @@ -0,0 +1,37 @@ +"""Generate the code reference pages. +(adapted from https://mkdocstrings.github.io/recipes/#bind-pages-to-sections-themselves) +""" + +from pathlib import Path + +import mkdocs_gen_files + +nav = mkdocs_gen_files.nav.Nav() + +root = Path(__file__).parent.parent +src = root / "src" + +for path in sorted(src.rglob("*.py")): + module_path = path.relative_to(src).with_suffix("") + doc_path = path.relative_to(src).with_suffix(".md") + full_doc_path = Path("reference", doc_path) + + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") + full_doc_path = full_doc_path.with_name("index.md") + elif parts[-1] == "__main__": + continue + + nav[parts] = doc_path.as_posix() + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + ident = ".".join(parts) + fd.write(f"::: {ident}") + + mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) + +with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as nav_file: + nav_file.writelines(nav.build_literate_nav()) diff --git a/scripts/pdoc/create_pydantic_patch.sh b/scripts/pdoc/create_pydantic_patch.sh deleted file mode 100755 index 05b6da6b..00000000 --- a/scripts/pdoc/create_pydantic_patch.sh +++ /dev/null @@ -1,25 +0,0 @@ -pydantic_root=$(python -c "import pydantic;from pathlib import Path;print(Path(pydantic.__file__).parent)") -main=$pydantic_root'/main.py' -original="$(dirname "$0")/original.py" -patched="$(dirname "$0")/patched.py" - -if [ -e $original ] -then - echo "found existing $original" -else - cp --verbose $main $original -fi - -if [ -e $patched ] -then - echo "found existing $patched" -else - cp --verbose $main $patched - echo "Please update $patched, then press enter to continue" - read -fi - -patch_file="$(dirname "$0")/mark_pydantic_attrs_private.patch" -diff -au $original $patched > $patch_file -echo "content of $patch_file:" -cat $patch_file diff --git a/scripts/pdoc/mark_pydantic_attrs_private.patch b/scripts/pdoc/mark_pydantic_attrs_private.patch deleted file mode 100644 index 722d4fbb..00000000 --- a/scripts/pdoc/mark_pydantic_attrs_private.patch +++ /dev/null @@ -1,28 +0,0 @@ ---- ./original.py 2024-11-08 15:18:37.493768700 +0100 -+++ ./patched.py 2024-11-08 15:13:54.288887700 +0100 -@@ -121,14 +121,14 @@ - # `GenerateSchema.model_schema` to work for a plain `BaseModel` annotation. - - model_config: ClassVar[ConfigDict] = ConfigDict() -- """ -+ """@private - Configuration for the model, should be a dictionary conforming to [`ConfigDict`][pydantic.config.ConfigDict]. - """ - - # Because `dict` is in the local namespace of the `BaseModel` class, we use `Dict` for annotations. - # TODO v3 fallback to `dict` when the deprecated `dict` method gets removed. - model_fields: ClassVar[Dict[str, FieldInfo]] = {} # noqa: UP006 -- """ -+ """@private - Metadata about the fields defined on the model, - mapping of field names to [`FieldInfo`][pydantic.fields.FieldInfo] objects. - -@@ -136,7 +136,7 @@ - """ - - model_computed_fields: ClassVar[Dict[str, ComputedFieldInfo]] = {} # noqa: UP006 -- """A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.""" -+ """@private A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.""" - - __class_vars__: ClassVar[set[str]] - """The names of the class variables defined on the model.""" diff --git a/scripts/pdoc/run.sh b/scripts/pdoc/run.sh deleted file mode 100755 index 74981aa5..00000000 --- a/scripts/pdoc/run.sh +++ /dev/null @@ -1,16 +0,0 @@ -cd "$(dirname "$0")" # cd to folder this script is in - -# patch pydantic to hide pydantic attributes that somehow show up in the docs -# (not even as inherited, but as if the documented class itself would define them) -pydantic_main=$(python -c "import pydantic;from pathlib import Path;print(Path(pydantic.__file__).parent / 'main.py')") - -patch --verbose --forward -p1 $pydantic_main < mark_pydantic_attrs_private.patch - -cd ../.. # cd to repo root -pdoc \ - --docformat google \ - --logo "https://bioimage.io/static/img/bioimage-io-logo.svg" \ - --logo-link "https://bioimage.io/" \ - --favicon "https://bioimage.io/static/img/bioimage-io-icon-small.svg" \ - --footer-text "bioimageio.core $(python -c 'import bioimageio.core;print(bioimageio.core.__version__)')" \ - -o ./dist bioimageio.core bioimageio.spec # generate bioimageio.spec as well for references diff --git a/src/bioimageio/core/cli.py b/src/bioimageio/core/cli.py index ff24f1ec..d0e09c84 100644 --- a/src/bioimageio/core/cli.py +++ b/src/bioimageio/core/cli.py @@ -16,6 +16,7 @@ from pathlib import Path from pprint import pformat, pprint from typing import ( + Annotated, Any, Dict, Iterable, @@ -30,24 +31,8 @@ Union, ) -import rich.markdown -from loguru import logger -from pydantic import AliasChoices, BaseModel, Field, model_validator -from pydantic_settings import ( - BaseSettings, - CliPositionalArg, - CliSettingsSource, - CliSubCommand, - JsonConfigSettingsSource, - PydanticBaseSettingsSource, - SettingsConfigDict, - YamlConfigSettingsSource, -) -from tqdm import tqdm -from typing_extensions import assert_never - import bioimageio.spec -from bioimageio.core import __version__ +import rich.markdown from bioimageio.spec import ( AnyModelDescr, InvalidDescr, @@ -65,6 +50,22 @@ from bioimageio.spec.model import ModelDescr, v0_4, v0_5 from bioimageio.spec.notebook import NotebookDescr from bioimageio.spec.utils import ensure_description_is_model, get_reader, write_yaml +from loguru import logger +from pydantic import AliasChoices, BaseModel, Field, PlainSerializer, model_validator +from pydantic_settings import ( + BaseSettings, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + JsonConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, + YamlConfigSettingsSource, +) +from tqdm import tqdm +from typing_extensions import assert_never + +from bioimageio.core import __version__ from .commands import WeightFormatArgAll, WeightFormatArgAny, package, test from .common import MemberId, SampleId, SupportedWeightsFormat @@ -450,7 +451,9 @@ class PredictCmd(CmdBase, WithSource): blockwise: bool = False """process inputs blockwise""" - stats: Path = Path("dataset_statistics.json") + stats: Annotated[Path, PlainSerializer(lambda p: p.as_posix())] = Path( + "dataset_statistics.json" + ) """path to dataset statistics (will be written if it does not exist, but the model requires statistical dataset measures) diff --git a/src/bioimageio/core/tensor.py b/src/bioimageio/core/tensor.py index 17358b00..c49469f7 100644 --- a/src/bioimageio/core/tensor.py +++ b/src/bioimageio/core/tensor.py @@ -177,11 +177,11 @@ def from_numpy( Args: array: the nd numpy array - axes: A description of the array's axes, + dims: A description of the array's axes, if None axes are guessed (which might fail and raise a ValueError.) Raises: - ValueError: if `axes` is None and axes guessing fails. + ValueError: if `dims` is None and dims guessing fails. """ if dims is None: diff --git a/src/bioimageio/core/weight_converters/_add_weights.py b/src/bioimageio/core/weight_converters/_add_weights.py index cc915619..255aa7b2 100644 --- a/src/bioimageio/core/weight_converters/_add_weights.py +++ b/src/bioimageio/core/weight_converters/_add_weights.py @@ -30,8 +30,8 @@ def add_weights( Default: choose automatically from any available. target_format: convert to a specific weights format. Default: attempt to convert to any missing format. - devices: Devices that may be used during conversion. verbose: log more (error) output + allow_tracing: allow conversion to torchscript by tracing if scripting fails. Returns: A (potentially invalid) model copy stored at `output_path` with added weights if any conversion was possible. From add1a416feff9148b5f451294c88846463c5c7d4 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 24 Nov 2025 14:21:32 +0100 Subject: [PATCH 02/22] use max disk space action --- .github/actions/max_disk_space/action.yaml | 13 +++++++++++++ .github/workflows/build.yaml | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 .github/actions/max_disk_space/action.yaml diff --git a/.github/actions/max_disk_space/action.yaml b/.github/actions/max_disk_space/action.yaml new file mode 100644 index 00000000..759e53af --- /dev/null +++ b/.github/actions/max_disk_space/action.yaml @@ -0,0 +1,13 @@ +name: 'Maximize disk space' +description: 'Maximize available disk space by removing unwanted software' + +runs: + using: 'composite' + steps: + - name: Maximize available disk space + uses: AdityaGarg8/remove-unwanted-software@v5 + with: + remove-android: 'true' + remove-dotnet: 'true' + remove-haskell: 'true' + remove-codeql: 'true' diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1e67773a..fdf78947 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -41,6 +41,8 @@ jobs: lookup-only: true - uses: actions/checkout@v4 if: steps.look-up.outputs.cache-hit != 'true' + - uses: ./.github/actions/max_disk_space + if: steps.look-up.outputs.cache-hit != 'true' - uses: actions/cache@v4 if: steps.look-up.outputs.cache-hit != 'true' with: @@ -219,6 +221,7 @@ jobs: with: fetch-depth: 0 fetch-tags: true + - uses: ./.github/actions/max_disk_space - uses: actions/download-artifact@v4 with: name: coverage-summary From 0fa1ef33067055bfda100ee276c6952ad17e6341 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 24 Nov 2025 14:48:53 +0100 Subject: [PATCH 03/22] use max disk space action in tests too --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fdf78947..db4f8c33 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -86,6 +86,7 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: ./.github/actions/max_disk_space - uses: actions/setup-python@v6 with: python-version: ${{matrix.python-version}} From e12e2e73912e3d94e0bfe51dbc6eed61409f5b4c Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 24 Nov 2025 15:18:01 +0100 Subject: [PATCH 04/22] update pyright settings --- .github/workflows/build.yaml | 2 ++ pyproject.toml | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index db4f8c33..8b16fe53 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -232,6 +232,8 @@ jobs: python-version: '3.12' cache: 'pip' - run: pip install -e .[dev,docs,partners] + - name: Check doc scripts + run: pyright scripts/generate_api_doc_pages.py - name: Get branch name to deploy to id: get_branch shell: bash diff --git a/pyproject.toml b/pyproject.toml index 6157b87e..03b135c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,8 +102,7 @@ exclude = [ "**/node_modules", "dogfood", "presentations", - "scripts/pdoc/original.py", - "scripts/pdoc/patched.py", + "scripts/generate_api_doc_pages.py", "tests/old_*", ] include = ["src", "scripts", "tests"] From 244ac487436f1e521982c25f959a7f43653ae803 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Tue, 25 Nov 2025 15:08:21 +0100 Subject: [PATCH 05/22] add tests --- tests/test_add_weights.py | 90 ++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/tests/test_add_weights.py b/tests/test_add_weights.py index 836353c7..86abd4a5 100644 --- a/tests/test_add_weights.py +++ b/tests/test_add_weights.py @@ -1,48 +1,42 @@ -# TODO: update add weights tests -# import os - - -# def _test_add_weights(model, tmp_path, base_weights, added_weights, **kwargs): -# from bioimageio.core.build_spec import add_weights - -# rdf = load_raw_resource_description(model) -# assert base_weights in rdf.weights -# assert added_weights in rdf.weights - -# weight_path = load_description(model).weights[added_weights].source -# assert weight_path.exists() - -# drop_weights = set(rdf.weights.keys()) - {base_weights} -# for drop in drop_weights: -# rdf.weights.pop(drop) -# assert tuple(rdf.weights.keys()) == (base_weights,) - -# in_path = tmp_path / "model1.zip" -# export_resource_package(rdf, output_path=in_path) - -# out_path = tmp_path / "model2.zip" -# add_weights(in_path, weight_path, weight_type=added_weights, output_path=out_path, **kwargs) - -# assert out_path.exists() -# new_rdf = load_description(out_path) -# assert set(new_rdf.weights.keys()) == {base_weights, added_weights} -# for weight in new_rdf.weights.values(): -# assert weight.source.exists() - -# test_res = _test_model(out_path, added_weights) -# failed = [s for s in test_res if s["status"] != "passed"] -# assert not failed, failed -# test_res = _test_model(out_path) -# failed = [s for s in test_res if s["status"] != "passed"] -# assert not failed, failed - -# # make sure the weights were cleaned from the cwd -# assert not os.path.exists(os.path.split(weight_path)[1]) - - -# def test_add_torchscript(unet2d_nuclei_broad_model, tmp_path): -# _test_add_weights(unet2d_nuclei_broad_model, tmp_path, "pytorch_state_dict", "torchscript") - - -# def test_add_onnx(unet2d_nuclei_broad_model, tmp_path): -# _test_add_weights(unet2d_nuclei_broad_model, tmp_path, "pytorch_state_dict", "onnx", opset_version=12) +import os +from pathlib import Path + +import pytest +from bioimageio.spec.model.v0_5 import WeightsFormat + +from bioimageio.core import add_weights, load_model_description + + +@pytest.mark.parametrize( + ("model_fixture", "source_format", "target_format"), + [ + ("unet2d_nuclei_broad_model", "pytorch_state_dict", "torchscript"), + ("unet2d_nuclei_broad_model", "pytorch_state_dict", "onnx"), + ("unet2d_nuclei_broad_model", "torchscript", "onnx"), + ], +) +def test_add_weights( + model_fixture: str, + source_format: WeightsFormat, + target_format: WeightsFormat, + tmp_path: Path, + request: pytest.FixtureRequest, +): + model_source = request.getfixturevalue(model_fixture) + + model = load_model_description(model_source, format_version="latest") + assert source_format in model.weights.available_formats, ( + "source format not found in model" + ) + if target_format in model.weights.available_formats: + model.weights[target_format] = None + + out_path = tmp_path / "converted.zip" + converted = add_weights( + model, + output_path=out_path, + source_format=source_format, + target_format=target_format, + ) + + assert target_format in converted.weights.available_formats From 20e44660dc29675d0370d24c34680201607a0e33 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Tue, 25 Nov 2025 15:08:33 +0100 Subject: [PATCH 06/22] fix typo --- src/bioimageio/core/commands.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/bioimageio/core/commands.py b/src/bioimageio/core/commands.py index 61d0bd4b..1a391f17 100644 --- a/src/bioimageio/core/commands.py +++ b/src/bioimageio/core/commands.py @@ -4,8 +4,6 @@ from pathlib import Path from typing import Optional, Sequence, Union -from typing_extensions import Literal - from bioimageio.spec import ( InvalidDescr, ResourceDescr, @@ -13,6 +11,7 @@ save_bioimageio_package_as_folder, ) from bioimageio.spec._internal.types import FormatVersionPlaceholder +from typing_extensions import Literal from ._resource_tests import test_description @@ -102,7 +101,7 @@ def package( Args: descr: a bioimageio resource description path: output path - weight-format: include only this single weight-format (if not 'all'). + weight_format: include only this single weight-format (if not 'all'). """ if isinstance(descr, InvalidDescr): logged = descr.validation_summary.save() From 2c34d6f90aa563c4364020ee2937d67b4641fb44 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Tue, 25 Nov 2025 15:08:57 +0100 Subject: [PATCH 07/22] WIP fix python repl examples in docs --- mkdocs.yaml | 17 ++++++++++++----- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mkdocs.yaml b/mkdocs.yaml index fb062233..7afb9714 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -86,29 +86,36 @@ plugins: backlinks: tree docstring_options: ignore_init_summary: true + returns_multiple_items: false + returns_named_value: false + trim_doctest_flags: true docstring_section_style: spacy + docstring_style: google filters: 'public' heading_level: 1 + imported_members: false inherited_members: true members: true merge_init_into_class: true parameter_headings: true - separate_signature: true + preload_modules: [pydantic, bioimageio.spec] scoped_crossrefs: true - show_root_heading: true + separate_signature: true + show_docstring_examples: true + show_if_no_docstring: true + show_inheritance_diagram: true show_root_full_path: false + show_root_heading: true show_signature_annotations: true - show_if_no_docstring: true show_source: true show_symbol_type_heading: true show_symbol_type_toc: true - # unwrap_annotated: true signature_crossrefs: true summary: true + # unwrap_annotated: true extensions: - griffe_pydantic: schema: true - show_inheritance_diagram: true - mike: alias_type: symlink canonical_version: latest diff --git a/pyproject.toml b/pyproject.toml index 03b135c0..3e5c9e91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ docs = [ "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-literate-nav", + "mkdocs-literate-nav", "mkdocs-material", "mkdocs-section-index", ] From 6b6c1567d24856d16f9f69686c75c03ff4675583 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Tue, 25 Nov 2025 15:33:18 +0100 Subject: [PATCH 08/22] fix issues in test_add_weights.py --- tests/test_add_weights.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_add_weights.py b/tests/test_add_weights.py index 86abd4a5..25e2ac56 100644 --- a/tests/test_add_weights.py +++ b/tests/test_add_weights.py @@ -1,4 +1,3 @@ -import os from pathlib import Path import pytest @@ -29,7 +28,7 @@ def test_add_weights( "source format not found in model" ) if target_format in model.weights.available_formats: - model.weights[target_format] = None + setattr(model.weights, target_format, None) out_path = tmp_path / "converted.zip" converted = add_weights( @@ -38,5 +37,8 @@ def test_add_weights( source_format=source_format, target_format=target_format, ) - + assert not isinstance(converted, InvalidDescr), ( + "conversion resulted in invalid descr", + converted.validation_summary.display(), + ) assert target_format in converted.weights.available_formats From 9bc83a15a63abd76be0ae265aff9d32e6bf100ad Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 8 Dec 2025 11:28:34 +0100 Subject: [PATCH 09/22] add missing import --- tests/test_add_weights.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_add_weights.py b/tests/test_add_weights.py index 25e2ac56..225257d6 100644 --- a/tests/test_add_weights.py +++ b/tests/test_add_weights.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from bioimageio.spec import InvalidDescr from bioimageio.spec.model.v0_5 import WeightsFormat from bioimageio.core import add_weights, load_model_description From 3f4c2512f3bea194ec32380e29b2cd2fe05953dd Mon Sep 17 00:00:00 2001 From: fynnbe Date: Tue, 9 Dec 2025 10:59:40 +0100 Subject: [PATCH 10/22] update docs --- README.md | 12 +++---- mkdocs.yaml | 8 ++++- scripts/generate_api_doc_pages.py | 38 +++++++++++++++++++-- src/bioimageio/core/__init__.py | 17 +++++++-- src/bioimageio/core/_prediction_pipeline.py | 5 ++- 5 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index da957da3..dc1f4b98 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,7 @@ bioimage.core has to offer: 1. test a model ```console - $ bioimageio test powerful-chipmunk - ... + bioimageio test powerful-chipmunk ```
@@ -65,8 +64,7 @@ bioimage.core has to offer: or ```console - $ bioimageio test impartial-shrimp - ... + bioimageio test impartial-shrimp ```
(Click to expand output) @@ -144,8 +142,7 @@ bioimage.core has to offer: - display the `bioimageio-predict` command help to get an overview: ```console - $ bioimageio predict --help - ... + bioimageio predict --help ```
@@ -233,8 +230,7 @@ bioimage.core has to offer: - create an example and run prediction locally! ```console - $ bioimageio predict impartial-shrimp --example=True - ... + bioimageio predict impartial-shrimp --example=True ```
diff --git a/mkdocs.yaml b/mkdocs.yaml index 7afb9714..bb61f13f 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -9,6 +9,7 @@ edit_uri: edit/main/docs/ theme: name: material + language: en features: - announce.dismiss - content.action.edit @@ -75,6 +76,9 @@ plugins: html_report_dir: dist/coverage - markdown-exec - mkdocstrings: + enable_inventory: true + default_handler: python + locale: en handlers: python: inventories: @@ -91,7 +95,7 @@ plugins: trim_doctest_flags: true docstring_section_style: spacy docstring_style: google - filters: 'public' + filters: ['!^_[^_]'] heading_level: 1 imported_members: false inherited_members: true @@ -155,6 +159,8 @@ markdown_extensions: - pymdownx.tilde - toc: permalink: '¤' + permalink_title: Anchor link to this section for reference + toc_depth: 2 nav: - Home: diff --git a/scripts/generate_api_doc_pages.py b/scripts/generate_api_doc_pages.py index a4ab1a82..bebf3e23 100644 --- a/scripts/generate_api_doc_pages.py +++ b/scripts/generate_api_doc_pages.py @@ -11,6 +11,9 @@ root = Path(__file__).parent.parent src = root / "src" +# Track flat nav entries we have added +added_nav_labels: set[str] = set() + for path in sorted(src.rglob("*.py")): module_path = path.relative_to(src).with_suffix("") doc_path = path.relative_to(src).with_suffix(".md") @@ -18,6 +21,10 @@ parts = tuple(module_path.parts) + # Skip if this is just the bioimageio namespace package + if parts == ("bioimageio",): + continue + if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") @@ -25,10 +32,37 @@ elif parts[-1] == "__main__": continue - nav[parts] = doc_path.as_posix() + if not parts: # Skip if parts is empty + continue + + # Build a flat nav for API Reference: one entry for bioimageio.core and + # one entry per top-level submodule under bioimageio.core. No subsections. + if parts[0:2] == ("bioimageio", "core"): + if len(parts) == 2: + # Landing page for bioimageio.core at reference/index.md + full_doc_path = Path("reference", "index.md") + doc_path = Path("index.md") + if "bioimageio.core" not in added_nav_labels: + nav[("bioimageio.core",)] = doc_path.as_posix() + added_nav_labels.add("bioimageio.core") + else: + # Top-level submodule/package directly under bioimageio.core + top = parts[2] + if top not in added_nav_labels: + pkg_init = src / "bioimageio" / "core" / top / "__init__.py" + if pkg_init.exists(): + nav_target = Path("bioimageio") / "core" / top / "index.md" + else: + nav_target = Path("bioimageio") / "core" / f"{top}.md" + + nav[(top,)] = nav_target.as_posix() + added_nav_labels.add(top) with mkdocs_gen_files.open(full_doc_path, "w") as fd: - ident = ".".join(parts) + # Reconstruct the full identifier from the original module_path + ident = ".".join(module_path.parts) + if ident.endswith(".__init__"): + ident = ident[:-9] # Remove .__init__ fd.write(f"::: {ident}") mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) diff --git a/src/bioimageio/core/__init__.py b/src/bioimageio/core/__init__.py index ac51907d..bab4a098 100644 --- a/src/bioimageio/core/__init__.py +++ b/src/bioimageio/core/__init__.py @@ -1,5 +1,18 @@ -""" -.. include:: ../../README.md +"""bioimageio.core --- core functionality for BioImage.IO resources + +The main focus on this library is to provide functionality to run prediction with +BioImage.IO models, including standardized pre- and postprocessing operations. +The BioImage.IO models (and other resources) are described by---and can be loaded with---the bioimageio.spec package. + +See `predict` and `predict_many` for straight-forward model inference +and `create_prediction_pipeline` for finer control of the inference process. + +Other notable bioimageio.core functionalities include: +- Testing BioImage.IO resources beyond format validation, e.g. by generating model outputs from test inputs. + See `test_model` or for arbitrary resource types `test_description`. +- Extending available model weight formats by converting existing ones, see `add_weights`. +- Creating and manipulating `Sample`s consisting of tensors with associated statistics. +- Computing statistics on datasets (represented as sequences of samples), see `compute_dataset_measures`. """ # ruff: noqa: E402 diff --git a/src/bioimageio/core/_prediction_pipeline.py b/src/bioimageio/core/_prediction_pipeline.py index 0b7717aa..a2b055bc 100644 --- a/src/bioimageio/core/_prediction_pipeline.py +++ b/src/bioimageio/core/_prediction_pipeline.py @@ -12,11 +12,10 @@ Union, ) +from bioimageio.spec.model import AnyModelDescr, v0_4, v0_5 from loguru import logger from tqdm import tqdm -from bioimageio.spec.model import AnyModelDescr, v0_4, v0_5 - from ._op_base import BlockedOperator from .axis import AxisId, PerAxis from .common import ( @@ -66,7 +65,7 @@ def __init__( default_blocksize_parameter: BlocksizeParameter = 10, default_batch_size: int = 1, ) -> None: - """Use `create_prediction_pipeline` to create a `PredictionPipeline`""" + """Consider using `create_prediction_pipeline` to create a `PredictionPipeline` with sensible defaults.""" super().__init__() default_blocksize_parameter = default_ns or default_blocksize_parameter if default_ns is not None: From 0aae9248f18d4b526620b063bd89f0ff999b90af Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 19 Dec 2025 11:31:03 +0100 Subject: [PATCH 11/22] mark bioimageio imports as known third party --- pyproject.toml | 3 +++ src/bioimageio/core/_prediction_pipeline.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e5c9e91..3d3ed5b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,5 +147,8 @@ exclude = [ [tool.ruff.lint] select = ["NPY201"] +[tool.ruff.lint.isort] +known-first-party = ["bioimageio"] + [tool.coverage.report] exclude_also = ["if TYPE_CHECKING:", "assert_never\\("] diff --git a/src/bioimageio/core/_prediction_pipeline.py b/src/bioimageio/core/_prediction_pipeline.py index a2b055bc..0cad757e 100644 --- a/src/bioimageio/core/_prediction_pipeline.py +++ b/src/bioimageio/core/_prediction_pipeline.py @@ -12,10 +12,11 @@ Union, ) -from bioimageio.spec.model import AnyModelDescr, v0_4, v0_5 from loguru import logger from tqdm import tqdm +from bioimageio.spec.model import AnyModelDescr, v0_4, v0_5 + from ._op_base import BlockedOperator from .axis import AxisId, PerAxis from .common import ( From 2e59dd413ffa0175031ccf4daa8b1b8137dc0515 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 19 Dec 2025 13:07:45 +0100 Subject: [PATCH 12/22] update absolute tolerance --- src/bioimageio/core/_resource_tests.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/bioimageio/core/_resource_tests.py b/src/bioimageio/core/_resource_tests.py index c4572929..c581d9ee 100644 --- a/src/bioimageio/core/_resource_tests.py +++ b/src/bioimageio/core/_resource_tests.py @@ -24,6 +24,10 @@ ) import numpy as np +from loguru import logger +from numpy.typing import NDArray +from typing_extensions import NotRequired, TypedDict, Unpack, assert_never, get_args + from bioimageio.spec import ( AnyDatasetDescr, AnyModelDescr, @@ -61,18 +65,14 @@ ValidationSummary, WarningEntry, ) -from loguru import logger -from numpy.typing import NDArray -from typing_extensions import NotRequired, TypedDict, Unpack, assert_never, get_args - -from bioimageio.core import __version__ -from bioimageio.core.io import save_tensor +from . import __version__ from ._prediction_pipeline import create_prediction_pipeline from ._settings import settings from .axis import AxisId, BatchSize from .common import MemberId, SupportedWeightsFormat from .digest_spec import get_test_input_sample, get_test_output_sample +from .io import save_tensor from .sample import Sample CONDA_CMD = "conda.bat" if platform.system() == "Windows" else "conda" @@ -710,7 +710,7 @@ def _get_tolerance( if wf == weights_format: applicable = v0_5.ReproducibilityTolerance( relative_tolerance=test_kwargs.get("relative_tolerance", 1e-3), - absolute_tolerance=test_kwargs.get("absolute_tolerance", 1e-4), + absolute_tolerance=test_kwargs.get("absolute_tolerance", 1e-3), ) break @@ -739,7 +739,7 @@ def _get_tolerance( mismatched_tol = 0 else: # use given (deprecated) test kwargs - atol = deprecated.get("absolute_tolerance", 1e-5) + atol = deprecated.get("absolute_tolerance", 1e-3) rtol = deprecated.get("relative_tolerance", 1e-3) mismatched_tol = 0 @@ -874,10 +874,10 @@ def add_warning_entry(msg: str): f"Output '{m}' disagrees with {mismatched_elements} of" + f" {expected_np.size} expected values" + f" ({mismatched_ppm:.1f} ppm)." - + f"\n Max relative difference: {r_max:.2e}" + + f"\n Max relative difference not accounted for by absolute tolerance ({atol:.2e}): {r_max:.2e}" + rf" (= \|{r_actual:.2e} - {r_expected:.2e}\|/\|{r_expected:.2e} + 1e-6\|)" + f" at {dict(zip(dims, r_max_idx))}" - + f"\n Max absolute difference not accounted for by relative tolerance: {a_max:.2e}" + + f"\n Max absolute difference not accounted for by relative tolerance ({rtol:.2e}): {a_max:.2e}" + rf" (= \|{a_actual:.7e} - {a_expected:.7e}\|) at {dict(zip(dims, a_max_idx))}" + f"\n Saved actual output to {actual_output_path}." ) From d9a2a0cfd0145c17b16478d79bae6eed93f7d2cd Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 19 Dec 2025 13:09:41 +0100 Subject: [PATCH 13/22] update docs --- mkdocs.yaml | 2 ++ pyproject.toml | 15 ++++++++------- scripts/generate_api_doc_pages.py | 7 +++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/mkdocs.yaml b/mkdocs.yaml index bb61f13f..f6c3c4d4 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -120,6 +120,8 @@ plugins: extensions: - griffe_pydantic: schema: true + - griffe_inherited_docstrings + - griffe_public_redundant_aliases - mike: alias_type: symlink canonical_version: latest diff --git a/pyproject.toml b/pyproject.toml index 3d3ed5b7..c5352c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,13 +49,16 @@ partners = [ # "stardist", # for model testing and stardist postprocessing # TODO: add updated stardist to partners env ] dev = [ - "cellpose", # for model testing + "cellpose", # for model testing "crick", + "griffe-pydantic", + "griffe-inherited-docstrings", + "griffe-public-redundant-aliases", "httpx", "jupyter", "keras>=3.0,<4", "matplotlib", - "monai", # for model testing + "monai", # for model testing "numpy", "onnx", "onnxruntime", @@ -66,23 +69,21 @@ dev = [ "pytest-cov", "pytest", "python-dotenv", - "segment-anything", # for model testing + "segment-anything", # for model testing "tensorflow", - "timm", # for model testing + "timm", # for model testing "torch>=1.6,<3", "torchvision>=0.21", ] docs = [ - "griffe-pydantic", "markdown-callouts", "markdown-exec", + "markdown-pycon", "mike", "mkdocs-api-autonav", "mkdocs-coverage", "mkdocs-gen-files", "mkdocs-literate-nav", - "mkdocs-literate-nav", - "mkdocs-literate-nav", "mkdocs-material", "mkdocs-section-index", ] diff --git a/scripts/generate_api_doc_pages.py b/scripts/generate_api_doc_pages.py index bebf3e23..872b9b29 100644 --- a/scripts/generate_api_doc_pages.py +++ b/scripts/generate_api_doc_pages.py @@ -25,6 +25,12 @@ if parts == ("bioimageio",): continue + # Skip private submodules prefixed with '_' + if any( + part.startswith("_") and part not in ("__init__", "__main__") for part in parts + ): + continue + if parts[-1] == "__init__": parts = parts[:-1] doc_path = doc_path.with_name("index.md") @@ -64,6 +70,7 @@ if ident.endswith(".__init__"): ident = ident[:-9] # Remove .__init__ fd.write(f"::: {ident}") + print(f"Written {full_doc_path}") mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root)) From b0b7ccdd3a671a324fbb8a106ead66b40e9cc1af Mon Sep 17 00:00:00 2001 From: fynnbe Date: Mon, 22 Dec 2025 16:01:56 +0100 Subject: [PATCH 14/22] update docs --- mkdocs.yaml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/mkdocs.yaml b/mkdocs.yaml index f6c3c4d4..ed97e53b 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -39,26 +39,25 @@ theme: - search.share - search.suggest - toc.follow - # - toc.integrate palette: - media: '(prefers-color-scheme)' - primary: 'deep-purple' - accent: 'blue' + primary: 'indigo' + accent: 'orange' toggle: icon: material/brightness-auto name: 'Switch to light mode' - media: '(prefers-color-scheme: light)' scheme: default - primary: 'deep-purple' - accent: 'blue' + primary: 'indigo' + accent: 'orange' toggle: icon: material/brightness-7 name: 'Switch to dark mode' - media: '(prefers-color-scheme: dark)' scheme: slate - primary: 'deep-purple' - accent: 'blue' + primary: 'indigo' + accent: 'orange' toggle: icon: material/brightness-4 name: 'Switch to system preference' @@ -93,13 +92,11 @@ plugins: returns_multiple_items: false returns_named_value: false trim_doctest_flags: true - docstring_section_style: spacy + # docstring_section_style: spacy docstring_style: google - filters: ['!^_[^_]'] + filters: public heading_level: 1 - imported_members: false inherited_members: true - members: true merge_init_into_class: true parameter_headings: true preload_modules: [pydantic, bioimageio.spec] @@ -112,11 +109,12 @@ plugins: show_root_heading: true show_signature_annotations: true show_source: true + show_submodules: true show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true - # unwrap_annotated: true + unwrap_annotated: false extensions: - griffe_pydantic: schema: true @@ -126,9 +124,6 @@ plugins: alias_type: symlink canonical_version: latest version_selector: true - - gen-files: - scripts: - - scripts/generate_api_doc_pages.py - literate-nav: nav_file: SUMMARY.md - search From e2d2807c8ceae245e51f4c9e3714546034ea6610 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Thu, 15 Jan 2026 10:23:43 +0100 Subject: [PATCH 15/22] avoid broken onnx_ir version --- pyproject.toml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c5352c6d..cd5cf288 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,11 @@ Documentation = "https://bioimage-io.github.io/core-bioimage-io-python/bioimagei Source = "https://github.com/bioimage-io/core-bioimage-io-python" [project.optional-dependencies] -onnx = ["onnxruntime", "onnxscript"] +onnx = [ + "onnxruntime", + "onnxscript", + 'onnx_ir!=0.1.14;python_version<"3.10"', # uses typing.Concatentate which requires py>=3.10 +] pytorch = ["torch>=1.6,<3", "torchvision>=0.21", "keras>=3.0,<4"] tensorflow = ["tensorflow", "keras>=2.15,<4"] partners = [ @@ -49,7 +53,7 @@ partners = [ # "stardist", # for model testing and stardist postprocessing # TODO: add updated stardist to partners env ] dev = [ - "cellpose", # for model testing + "cellpose", # for model testing "crick", "griffe-pydantic", "griffe-inherited-docstrings", @@ -58,20 +62,21 @@ dev = [ "jupyter", "keras>=3.0,<4", "matplotlib", - "monai", # for model testing + "monai", # for model testing "numpy", "onnx", "onnxruntime", "onnxscript", + 'onnx_ir!=0.1.14;python_version<"3.10"', # uses typing.Concatentate which requires py>=3.10 "packaging>=17.0", "pre-commit", "pyright==1.1.407", "pytest-cov", "pytest", "python-dotenv", - "segment-anything", # for model testing + "segment-anything", # for model testing "tensorflow", - "timm", # for model testing + "timm", # for model testing "torch>=1.6,<3", "torchvision>=0.21", ] From 28c6867b771d28d4cc6d3a123a010a0a059a3172 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Thu, 15 Jan 2026 10:37:19 +0100 Subject: [PATCH 16/22] bump patch and spec and update changelog --- changelog.md | 5 +++++ pyproject.toml | 2 +- src/bioimageio/core/__init__.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 1ce18e61..8172bb40 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,8 @@ +### 0.9.6 + +- bump bioimageio.spec library version to 0.5.6.0 +- increase default reprducibility tolerance + ### 0.9.5 - bump bioimageio.spec library version to 0.5.6.0 diff --git a/pyproject.toml b/pyproject.toml index cd5cf288..f0aa86ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires-python = ">=3.9" readme = "README.md" dynamic = ["version"] dependencies = [ - "bioimageio.spec ==0.5.6.0", + "bioimageio.spec ==0.5.7.0", "h5py", "imagecodecs", "imageio>=2.10", diff --git a/src/bioimageio/core/__init__.py b/src/bioimageio/core/__init__.py index bab4a098..78f43d46 100644 --- a/src/bioimageio/core/__init__.py +++ b/src/bioimageio/core/__init__.py @@ -16,7 +16,7 @@ """ # ruff: noqa: E402 -__version__ = "0.9.5" +__version__ = "0.9.6" from loguru import logger logger.disable("bioimageio.core") From ad6224735c843ddead33de2a2a9b6d9c5e75e984 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Thu, 15 Jan 2026 10:44:11 +0100 Subject: [PATCH 17/22] rewrite test_add_weights --- tests/test_add_weights.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/test_add_weights.py b/tests/test_add_weights.py index 225257d6..31932f4d 100644 --- a/tests/test_add_weights.py +++ b/tests/test_add_weights.py @@ -1,30 +1,28 @@ from pathlib import Path import pytest -from bioimageio.spec import InvalidDescr -from bioimageio.spec.model.v0_5 import WeightsFormat from bioimageio.core import add_weights, load_model_description +from bioimageio.spec import InvalidDescr +from bioimageio.spec.model.v0_5 import WeightsFormat @pytest.mark.parametrize( - ("model_fixture", "source_format", "target_format"), + ("source_format", "target_format"), [ - ("unet2d_nuclei_broad_model", "pytorch_state_dict", "torchscript"), - ("unet2d_nuclei_broad_model", "pytorch_state_dict", "onnx"), - ("unet2d_nuclei_broad_model", "torchscript", "onnx"), + ("pytorch_state_dict", "torchscript"), + ("pytorch_state_dict", "onnx"), + ("torchscript", "onnx"), ], ) def test_add_weights( - model_fixture: str, source_format: WeightsFormat, target_format: WeightsFormat, + unet2d_nuclei_broad_model: str, tmp_path: Path, request: pytest.FixtureRequest, ): - model_source = request.getfixturevalue(model_fixture) - - model = load_model_description(model_source, format_version="latest") + model = load_model_description(unet2d_nuclei_broad_model, format_version="latest") assert source_format in model.weights.available_formats, ( "source format not found in model" ) From 83c3244f59106723f04c5b531b837e003e61203f Mon Sep 17 00:00:00 2001 From: fynnbe Date: Thu, 15 Jan 2026 16:19:09 +0100 Subject: [PATCH 18/22] fix conda recipe generation --- conda-recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index b4ffb39a..1538cc25 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -55,7 +55,7 @@ test: requires: {% for dep in pyproject['project']['optional-dependencies']['dev'] %} {% if 'torch' not in dep %} # can't install pytorch>=2.8 from conda-forge smh - - {{ dep.lower().replace('_', '-') }} + - {{ dep.lower().replace('_', '-').replace('onnx_ir!=0.1.14;python_version<"3.10"', 'onnx_ir!=0.1.14') }} {% endif %} {% endfor %} commands: From f66efcdd07fd61cb856921354615f77270349e2a Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 16 Jan 2026 09:51:43 +0100 Subject: [PATCH 19/22] add tests --- tests/test_resource_tests.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_resource_tests.py b/tests/test_resource_tests.py index f4eca96b..5b09176b 100644 --- a/tests/test_resource_tests.py +++ b/tests/test_resource_tests.py @@ -1,3 +1,7 @@ +from pathlib import Path + +import numpy as np + from bioimageio.spec import InvalidDescr, ValidationContext @@ -42,3 +46,31 @@ def test_loading_description_multiple_times(unet2d_nuclei_broad_model: str): # load again, which some users might end up doing model_descr = load_description(model_descr) # pyright: ignore[reportArgumentType] assert not isinstance(model_descr, InvalidDescr) + + +def test_test_description_runtime_env(unet2d_nuclei_broad_model: str): + from bioimageio.core._resource_tests import test_description + + summary = test_description(unet2d_nuclei_broad_model, runtime_env="as-described") + + assert summary.status == "passed", summary.display() + + +def test_failed_reproducibility(unet2d_nuclei_broad_model: str, tmp_path: str): + from bioimageio.core import load_model + from bioimageio.core._resource_tests import test_model + from bioimageio.spec.common import FileDescr + from bioimageio.spec.utils import load_array, save_array + + model = load_model(unet2d_nuclei_broad_model, format_version="latest") + + # use corrupted test input to fail the reproducibility test + test_array_path = Path(tmp_path) / "input.npy" + assert model.inputs[0].test_tensor is not None + test_array = load_array(model.inputs[0].test_tensor) + save_array(test_array_path, np.zeros_like(test_array)) + model.inputs[0].test_tensor = FileDescr(source=test_array_path) + + summary = test_model(model) + + assert summary.status == "valid-format" From ce8a96340495ee7005476f487329ba34c9499124 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 16 Jan 2026 10:25:07 +0100 Subject: [PATCH 20/22] fix conda recipe --- conda-recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 1538cc25..a69a4e14 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -55,7 +55,7 @@ test: requires: {% for dep in pyproject['project']['optional-dependencies']['dev'] %} {% if 'torch' not in dep %} # can't install pytorch>=2.8 from conda-forge smh - - {{ dep.lower().replace('_', '-').replace('onnx_ir!=0.1.14;python_version<"3.10"', 'onnx_ir!=0.1.14') }} + - {{ dep.lower().replace('onnx_ir!=0.1.14;python_version<"3.10"', 'onnx-ir!=0.1.14').replace('_', '-') }} {% endif %} {% endfor %} commands: From c76434edabc9deb2f0e3f80258b4838990450b86 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 16 Jan 2026 11:39:35 +0100 Subject: [PATCH 21/22] improve cli --- pyproject.toml | 3 + src/bioimageio/core/__main__.py | 4 +- src/bioimageio/core/cli.py | 66 ++++++-------- tests/test_cli.py | 151 +++++++++++++++++--------------- 4 files changed, 113 insertions(+), 111 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f0aa86ab..9359682e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,3 +158,6 @@ known-first-party = ["bioimageio"] [tool.coverage.report] exclude_also = ["if TYPE_CHECKING:", "assert_never\\("] + +[tool.coverage.run] +patch = ["subprocess"] diff --git a/src/bioimageio/core/__main__.py b/src/bioimageio/core/__main__.py index ed7c3280..123b6a9c 100644 --- a/src/bioimageio/core/__main__.py +++ b/src/bioimageio/core/__main__.py @@ -1,6 +1,7 @@ import sys from loguru import logger +from pydantic_settings import CliApp logger.enable("bioimageio") @@ -17,8 +18,7 @@ def main(): - cli = Bioimageio() # pyright: ignore[reportCallIssue] - cli.run() + _ = CliApp.run(Bioimageio) if __name__ == "__main__": diff --git a/src/bioimageio/core/cli.py b/src/bioimageio/core/cli.py index d0e09c84..a6866ff0 100644 --- a/src/bioimageio/core/cli.py +++ b/src/bioimageio/core/cli.py @@ -31,8 +31,25 @@ Union, ) -import bioimageio.spec import rich.markdown +from loguru import logger +from pydantic import AliasChoices, BaseModel, Field, PlainSerializer, model_validator +from pydantic_settings import ( + BaseSettings, + CliApp, + CliPositionalArg, + CliSettingsSource, + CliSubCommand, + JsonConfigSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, + YamlConfigSettingsSource, +) +from tqdm import tqdm +from typing_extensions import assert_never + +import bioimageio.spec +from bioimageio.core import __version__ from bioimageio.spec import ( AnyModelDescr, InvalidDescr, @@ -50,22 +67,6 @@ from bioimageio.spec.model import ModelDescr, v0_4, v0_5 from bioimageio.spec.notebook import NotebookDescr from bioimageio.spec.utils import ensure_description_is_model, get_reader, write_yaml -from loguru import logger -from pydantic import AliasChoices, BaseModel, Field, PlainSerializer, model_validator -from pydantic_settings import ( - BaseSettings, - CliPositionalArg, - CliSettingsSource, - CliSubCommand, - JsonConfigSettingsSource, - PydanticBaseSettingsSource, - SettingsConfigDict, - YamlConfigSettingsSource, -) -from tqdm import tqdm -from typing_extensions import assert_never - -from bioimageio.core import __version__ from .commands import WeightFormatArgAll, WeightFormatArgAny, package, test from .common import MemberId, SampleId, SupportedWeightsFormat @@ -161,7 +162,7 @@ class ValidateFormatCmd(CmdBase, WithSource, WithSummaryLogging): def descr(self): return load_description(self.source, perform_io_checks=self.perform_io_checks) - def run(self): + def cli_cmd(self): self.log(self.descr) sys.exit( 0 @@ -213,7 +214,7 @@ class TestCmd(CmdBase, WithSource, WithSummaryLogging): - '0.4', '0.5', ...: Use the specified format version (may trigger auto updating) """ - def run(self): + def cli_cmd(self): sys.exit( test( self.descr, @@ -242,7 +243,7 @@ class PackageCmd(CmdBase, WithSource, WithSummaryLogging): ) """The weight format to include in the package (for model descriptions only).""" - def run(self): + def cli_cmd(self): if isinstance(self.descr, InvalidDescr): self.log(self.descr) raise ValueError(f"Invalid {self.descr.type} description.") @@ -315,7 +316,7 @@ class UpdateCmdBase(CmdBase, WithSource, ABC): def updated(self) -> Union[ResourceDescr, InvalidDescr]: raise NotImplementedError - def run(self): + def cli_cmd(self): original_yaml = open_bioimageio_yaml(self.source).unparsed_content assert isinstance(original_yaml, str) stream = StringIO() @@ -577,7 +578,7 @@ def get_example_command(preview: bool, escape: bool = False): + f"\n(note that a local '{JSON_FILE}' or '{YAML_FILE}' may interfere with this)" ) - def run(self): + def cli_cmd(self): if self.example: return self._example() @@ -745,6 +746,8 @@ def input_dataset(stat: Stat): class AddWeightsCmd(CmdBase, WithSource, WithSummaryLogging): + """Add additional weights to a model description by converting from available formats.""" + output: CliPositionalArg[Path] """The path to write the updated model package to.""" @@ -761,7 +764,7 @@ class AddWeightsCmd(CmdBase, WithSource, WithSummaryLogging): """Allow tracing when converting pytorch_state_dict to torchscript (still uses scripting if possible).""" - def run(self): + def cli_cmd(self): model_descr = ensure_description_is_model(self.descr) if isinstance(model_descr, v0_4.ModelDescr): raise TypeError( @@ -817,8 +820,7 @@ class Bioimageio( """Create a bioimageio.yaml description with updated file hashes.""" add_weights: CliSubCommand[AddWeightsCmd] = Field(alias="add-weights") - """Add additional weights to the model descriptions converted from available - formats to improve deployability.""" + """Add additional weights to a model description by converting from available formats.""" @classmethod def settings_customise_sources( @@ -852,22 +854,12 @@ def _log(cls, data: Any): ) return data - def run(self): + def cli_cmd(self) -> None: logger.info( "executing CLI command:\n{}", pformat({k: v for k, v in self.model_dump().items() if v is not None}), ) - cmd = ( - self.add_weights - or self.package - or self.predict - or self.test - or self.update_format - or self.update_hashes - or self.validate_format - ) - assert cmd is not None - cmd.run() + _ = CliApp.run_subcommand(self) assert isinstance(Bioimageio.__doc__, str) diff --git a/tests/test_cli.py b/tests/test_cli.py index 203677ec..fc805611 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2,6 +2,7 @@ from pathlib import Path from typing import Any, List, Sequence +import numpy as np import pytest from pydantic import FilePath @@ -21,6 +22,8 @@ def run_subprocess( @pytest.mark.parametrize( "args", [ + ["--help"], + ["add-weights", "unet2d_nuclei_broad_model", "tmp_path"], [ "package", "unet2d_nuclei_broad_model", @@ -29,6 +32,7 @@ def run_subprocess( "pytorch_state_dict", ], ["package", "unet2d_nuclei_broad_model", "output.zip"], + ["predict", "--example", "unet2d_nuclei_broad_model"], [ "test", "unet2d_nuclei_broad_model", @@ -36,11 +40,9 @@ def run_subprocess( "pytorch_state_dict", ], ["test", "unet2d_nuclei_broad_model"], - ["predict", "--example", "unet2d_nuclei_broad_model"], ["update-format", "unet2d_nuclei_broad_model_old"], - ["add-weights", "unet2d_nuclei_broad_model", "tmp_path"], - ["update-hashes", "unet2d_nuclei_broad_model_old"], ["update-hashes", "unet2d_nuclei_broad_model_old", "--output=stdout"], + ["update-hashes", "unet2d_nuclei_broad_model_old"], ], ) def test_cli( @@ -77,83 +79,88 @@ def test_cli_fails(args: List[str], stardist_wrong_shape: FilePath): assert ret.returncode == 1, ret.stdout -# TODO: update CLI test -# def _test_cli_predict_image(model: Path, tmp_path: Path, extra_cmd_args: Optional[List[str]] = None): -# spec = load_description(model) -# in_path = spec.test_inputs[0] - -# out_path = tmp_path.with_suffix(".npy") -# cmd = ["bioimageio", "predict-image", model, "--input", str(in_path), "--output", str(out_path)] -# if extra_cmd_args is not None: -# cmd.extend(extra_cmd_args) -# ret = run_subprocess(cmd) -# assert ret.returncode == 0, ret.stdout -# assert out_path.exists() - - -# def test_cli_predict_image(unet2d_nuclei_broad_model: Path, tmp_path: Path): -# _test_cli_predict_image(unet2d_nuclei_broad_model, tmp_path) - - -# def test_cli_predict_image_with_weight_format(unet2d_nuclei_broad_model: Path, tmp_path: Path): -# _test_cli_predict_image(unet2d_nuclei_broad_model, tmp_path, ["--weight-format", "pytorch_state_dict"]) - - -# def _test_cli_predict_images(model: Path, tmp_path: Path, extra_cmd_args: Optional[List[str]] = None): -# n_images = 3 -# shape = (1, 1, 128, 128) -# expected_shape = (1, 1, 128, 128) - -# in_folder = tmp_path / "inputs" -# in_folder.mkdir() -# out_folder = tmp_path / "outputs" -# out_folder.mkdir() - -# expected_outputs: List[Path] = [] -# for i in range(n_images): -# path = in_folder / f"im-{i}.npy" -# im = np.random.randint(0, 255, size=shape).astype("uint8") -# np.save(path, im) -# expected_outputs.append(out_folder / f"im-{i}.npy") - -# input_pattern = str(in_folder / "*.npy") -# cmd = ["bioimageio", "predict-images", str(model), input_pattern, str(out_folder)] -# if extra_cmd_args is not None: -# cmd.extend(extra_cmd_args) -# ret = run_subprocess(cmd) -# assert ret.returncode == 0, ret.stdout +def _test_cli_predict_single( + model_source: str, tmp_path: Path, extra_cmd_args: Sequence[str] = () +): + from bioimageio.spec import load_model_description + + model = load_model_description(model_source, format_version="latest") + assert model.inputs[0].test_tensor is not None + in_source = model.inputs[0].test_tensor.source + + out_path = tmp_path.with_suffix(".npy") + cmd = [ + "bioimageio", + "predict", + str(model_source), + "--input", + str(in_source), + "--output", + str(out_path), + ] + list(extra_cmd_args) + ret = run_subprocess(cmd) + assert ret.returncode == 0, ret.stdout + assert out_path.exists() -# for out_path in expected_outputs: -# assert out_path.exists() -# assert np.load(out_path).shape == expected_shape +def test_cli_predict_single(unet2d_nuclei_broad_model: Path, tmp_path: Path): + _test_cli_predict_single(str(unet2d_nuclei_broad_model), tmp_path) -# def test_cli_predict_images(unet2d_nuclei_broad_model: Path, tmp_path: Path): -# _test_cli_predict_images(unet2d_nuclei_broad_model, tmp_path) +def test_cli_predict_single_with_weight_format( + unet2d_nuclei_broad_model: Path, tmp_path: Path +): + _test_cli_predict_single( + str(unet2d_nuclei_broad_model), + tmp_path, + ["--weight-format", "pytorch_state_dict"], + ) -# def test_cli_predict_images_with_weight_format(unet2d_nuclei_broad_model: Path, tmp_path: Path): -# _test_cli_predict_images(unet2d_nuclei_broad_model, tmp_path, ["--weight-format", "pytorch_state_dict"]) +def _test_cli_predict_multiple( + model_source: str, tmp_path: Path, extra_cmd_args: Sequence[str] = () +): + n_images = 3 + shape = (1, 1, 128, 128) + expected_shape = (1, 1, 128, 128) + + in_folder = tmp_path / "inputs" + in_folder.mkdir() + out_folder = tmp_path / "outputs" + out_folder.mkdir() + + expected_outputs: List[Path] = [] + for i in range(n_images): + path = in_folder / f"im-{i}.npy" + im = np.random.randint(0, 255, size=shape).astype("uint8") + np.save(path, im) + expected_outputs.append(out_folder / f"im-{i}.npy") + + input_pattern = str(in_folder / "*.npy") + cmd = [ + "bioimageio", + "predict", + model_source, + input_pattern, + str(out_folder), + ] + list(extra_cmd_args) + ret = run_subprocess(cmd) + assert ret.returncode == 0, ret.stdout -# def test_torch_to_torchscript(unet2d_nuclei_broad_model: Path, tmp_path: Path): -# out_path = tmp_path.with_suffix(".pt") -# ret = run_subprocess( -# ["bioimageio", "convert-torch-weights-to-torchscript", str(unet2d_nuclei_broad_model), str(out_path)] -# ) -# assert ret.returncode == 0, ret.stdout -# assert out_path.exists() + for out_path in expected_outputs: + assert out_path.exists() + assert np.load(out_path).shape == expected_shape -# def test_torch_to_onnx(convert_to_onnx: Path, tmp_path: Path): -# out_path = tmp_path.with_suffix(".onnx") -# ret = run_subprocess(["bioimageio", "convert-torch-weights-to-onnx", str(convert_to_onnx), str(out_path)]) -# assert ret.returncode == 0, ret.stdout -# assert out_path.exists() +def test_cli_predict_multiple(unet2d_nuclei_broad_model: Path, tmp_path: Path): + _test_cli_predict_multiple(str(unet2d_nuclei_broad_model), tmp_path) -# def test_keras_to_tf(unet2d_keras: Path, tmp_path: Path): -# out_path = tmp_path / "weights.zip" -# ret = run_subprocess(["bioimageio", "convert-keras-weights-to-tensorflow", str(unet2d_keras), str(out_path)]) -# assert ret.returncode == 0, ret.stdout -# assert out_path.exists() +def test_cli_predict_multiple_with_weight_format( + unet2d_nuclei_broad_model: Path, tmp_path: Path +): + _test_cli_predict_multiple( + str(unet2d_nuclei_broad_model), + tmp_path, + ["--weight-format", "pytorch_state_dict"], + ) From df7eed15cdf4e4fcea9d8160e15cf59943645a17 Mon Sep 17 00:00:00 2001 From: fynnbe Date: Fri, 16 Jan 2026 13:02:38 +0100 Subject: [PATCH 22/22] fix cli tests --- src/bioimageio/core/cli.py | 10 +++++++++- tests/test_cli.py | 28 +++++++++++++++------------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/bioimageio/core/cli.py b/src/bioimageio/core/cli.py index a6866ff0..13aa6e21 100644 --- a/src/bioimageio/core/cli.py +++ b/src/bioimageio/core/cli.py @@ -659,7 +659,7 @@ def expand_outputs(): ) for s in sample_ids ] - + # check for distinctness and correct number within each output sample for i, out in enumerate(outputs, start=1): if len(set(out)) < len(out): raise ValueError( @@ -671,6 +671,14 @@ def expand_outputs(): f"[output sample #{i}] Expected {len(output_ids)} outputs {output_ids}, got {out}" ) + # check for distinctness across all output samples + all_output_paths = [p for out in outputs for p in out] + if len(set(all_output_paths)) < len(all_output_paths): + raise ValueError( + "Output paths are not distinct across samples. " + + f"Make sure to include '{{sample_id}}' in the output path pattern." + ) + return outputs outputs = expand_outputs() diff --git a/tests/test_cli.py b/tests/test_cli.py index fc805611..7e9de4ef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -86,16 +86,16 @@ def _test_cli_predict_single( model = load_model_description(model_source, format_version="latest") assert model.inputs[0].test_tensor is not None - in_source = model.inputs[0].test_tensor.source - - out_path = tmp_path.with_suffix(".npy") + in_path = tmp_path / "in.npy" + _ = in_path.write_bytes(model.inputs[0].test_tensor.get_reader().read()) + out_path = tmp_path / "out.npy" cmd = [ "bioimageio", "predict", str(model_source), - "--input", - str(in_source), - "--output", + "--inputs", + str(in_path), + "--outputs", str(out_path), ] + list(extra_cmd_args) ret = run_subprocess(cmd) @@ -128,21 +128,23 @@ def _test_cli_predict_multiple( in_folder.mkdir() out_folder = tmp_path / "outputs" out_folder.mkdir() - + out_file_pattern = "im-{sample_id}.npy" + inputs: List[str] = [] expected_outputs: List[Path] = [] for i in range(n_images): - path = in_folder / f"im-{i}.npy" + input_path = in_folder / f"im-{i}.npy" im = np.random.randint(0, 255, size=shape).astype("uint8") - np.save(path, im) - expected_outputs.append(out_folder / f"im-{i}.npy") + np.save(input_path, im) + inputs.extend(["--inputs", str(input_path)]) + expected_outputs.append(out_folder / out_file_pattern.format(sample_id=i)) - input_pattern = str(in_folder / "*.npy") cmd = [ "bioimageio", "predict", model_source, - input_pattern, - str(out_folder), + *inputs, + "--outputs", + str(out_folder / out_file_pattern), ] + list(extra_cmd_args) ret = run_subprocess(cmd) assert ret.returncode == 0, ret.stdout