diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eadbecbe9..7dc5ad9c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,7 @@ jobs: include: - python-version: "3.12" is-dev-version: true + run_expensive_tests: true steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -29,12 +30,13 @@ jobs: - name: Get Date id: get-date run: | - echo "week=$(/bin/date -u "+%Y-%U")" >> $GITHUB_OUTPUT + echo "date=$(date +'%Y-%b')" + echo "date=$(date +'%Y-%b')" >> $GITHUB_OUTPUT shell: bash - - uses: actions/cache@v3 + - uses: actions/cache/restore@v4 with: - path: tests/cache - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ steps.get-date.outputs.week }}-${{ hashFiles('**/lockfiles') }} + path: bioimageio_cache + key: "py${{ matrix.python-version }}-${{ steps.get-date.outputs.date }}" - name: Check autogenerated imports run: python scripts/generate_version_submodule_imports.py check - run: black --check . @@ -49,14 +51,22 @@ jobs: - run: pyright -p pyproject.toml --pythonversion ${{ matrix.python-version }} if: matrix.is-dev-version - run: pytest + env: + BIOIMAGEIO_CACHE_PATH: bioimageio_cache + SKIP_EXPENSIVE_TESTS: ${{ matrix.run_expensive_tests && 'false' || 'true' }} + - uses: actions/cache/save@v4 + # explicit restore/save instead of cache action to cache even if coverage fails + with: + path: bioimageio_cache + key: "py${{ matrix.python-version }}-${{ steps.get-date.outputs.date }}" - if: matrix.is-dev-version && github.event_name == 'pull_request' uses: orgoro/coverage@v3.2 with: coverageFile: coverage.xml token: ${{ secrets.GITHUB_TOKEN }} - thresholdAll: 0.75 + thresholdAll: 0.7 thresholdNew: 0.9 - thresholdModified: 0.85 + thresholdModified: 0.6 - if: matrix.is-dev-version run: | pip install genbadge[coverage] diff --git a/.gitignore b/.gitignore index 932ad78b5..51d58bef8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,9 @@ coverage.xml dist/ docs/ output/ -tests/cache tests/generated_json_schemas tmp/ user_docs/ scripts/pdoc/original.py scripts/pdoc/patched.py +bioimageio_cache/ diff --git a/README.md b/README.md index 85af3fb2a..7db4ec698 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ To keep the bioimageio.spec Python package version in sync with the (model) desc * fix summary formatting * improve logged origin for logged messages * make the `model.v0_5.ModelDescr.training_data` field a `left_to_right` Union to avoid warnings +* the deprecated `version_number` is no longer appended to the `id`, but instead set as `version` if no `version` is specified. #### bioimageio.spec 0.5.3.3 diff --git a/bioimageio/spec/application/v0_2.py b/bioimageio/spec/application/v0_2.py index fae82093b..f85a4aa17 100644 --- a/bioimageio/spec/application/v0_2.py +++ b/bioimageio/spec/application/v0_2.py @@ -33,7 +33,8 @@ class ApplicationDescr(GenericDescrBase, title="bioimage.io application specific type: Literal["application"] = "application" id: Optional[ApplicationId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" source: Annotated[ Optional[ImportantFileSource], diff --git a/bioimageio/spec/application/v0_3.py b/bioimageio/spec/application/v0_3.py index 42596532f..604c0c439 100644 --- a/bioimageio/spec/application/v0_3.py +++ b/bioimageio/spec/application/v0_3.py @@ -34,7 +34,8 @@ class ApplicationDescr(GenericDescrBase, title="bioimage.io application specific type: Literal["application"] = "application" id: Optional[ApplicationId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" parent: Optional[ApplicationId] = None """The description from which this one is derived""" diff --git a/bioimageio/spec/dataset/v0_2.py b/bioimageio/spec/dataset/v0_2.py index 13336ed59..a68b9330c 100644 --- a/bioimageio/spec/dataset/v0_2.py +++ b/bioimageio/spec/dataset/v0_2.py @@ -30,7 +30,8 @@ class DatasetDescr(GenericDescrBase, title="bioimage.io dataset specification"): type: Literal["dataset"] = "dataset" id: Optional[DatasetId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" source: Optional[HttpUrl] = None """"URL to the source of the dataset.""" diff --git a/bioimageio/spec/dataset/v0_3.py b/bioimageio/spec/dataset/v0_3.py index c51ec40a7..1cba38733 100644 --- a/bioimageio/spec/dataset/v0_3.py +++ b/bioimageio/spec/dataset/v0_3.py @@ -43,7 +43,8 @@ class DatasetDescr(GenericDescrBase, title="bioimage.io dataset specification"): type: Literal["dataset"] = "dataset" id: Optional[DatasetId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" parent: Optional[DatasetId] = None """The description from which this one is derived""" diff --git a/bioimageio/spec/generic/v0_2.py b/bioimageio/spec/generic/v0_2.py index 40d026209..3a9cca34b 100644 --- a/bioimageio/spec/generic/v0_2.py +++ b/bioimageio/spec/generic/v0_2.py @@ -450,7 +450,8 @@ class GenericDescr( """The resource type assigns a broad category to the resource.""" id: Optional[ResourceId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" source: Optional[HttpUrl] = None """The primary source of the resource""" diff --git a/bioimageio/spec/generic/v0_3.py b/bioimageio/spec/generic/v0_3.py index d69d5793d..552531581 100644 --- a/bioimageio/spec/generic/v0_3.py +++ b/bioimageio/spec/generic/v0_3.py @@ -367,8 +367,8 @@ def _remove_version_number( # pyright: ignore[reportUnknownParameterType] ): if isinstance(value, dict): vn: Any = value.pop("version_number", None) - if vn is not None and "id" in value: - value["id"] = f"{value['id']}/{vn}" + if vn is not None and value.get("version") is None: + value["version"] = vn return value # pyright: ignore[reportUnknownVariableType] @@ -420,7 +420,8 @@ class GenericDescr( """The resource type assigns a broad category to the resource.""" id: Optional[ResourceId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" parent: Optional[ResourceId] = None """The description from which this one is derived""" @@ -448,7 +449,10 @@ def _remove_version_number( # pyright: ignore[reportUnknownParameterType] ): if isinstance(value, dict): vn: Any = value.pop("version_number", None) - if vn is not None and "id" in value: - value["id"] = f"{value['id']}/{vn}" + if vn is not None and value.get("version") is None: + value["version"] = vn return value # pyright: ignore[reportUnknownVariableType] + + version: Optional[Version] = None + """The version of the linked resource following SemVer 2.0.""" diff --git a/bioimageio/spec/model/v0_4.py b/bioimageio/spec/model/v0_4.py index 34bc5760f..73780278e 100644 --- a/bioimageio/spec/model/v0_4.py +++ b/bioimageio/spec/model/v0_4.py @@ -917,7 +917,8 @@ class ModelDescr(GenericModelDescrBase, title="bioimage.io model specification") """Specialized resource type 'model'""" id: Optional[ModelId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" authors: NotEmpty[ # pyright: ignore[reportGeneralTypeIssues] # make mandatory List[Author] diff --git a/bioimageio/spec/model/v0_5.py b/bioimageio/spec/model/v0_5.py index 3dcd8140e..577b4969a 100644 --- a/bioimageio/spec/model/v0_5.py +++ b/bioimageio/spec/model/v0_5.py @@ -35,6 +35,7 @@ import numpy as np from annotated_types import Ge, Gt, Interval, MaxLen, MinLen, Predicate from imageio.v3 import imread, imwrite # pyright: ignore[reportUnknownVariableType] +from loguru import logger from numpy.typing import NDArray from pydantic import ( Discriminator, @@ -350,10 +351,20 @@ def get_size( SpaceOutputAxis, SpaceOutputAxisWithHalo, ], - n: ParameterizedSize_N, + n: ParameterizedSize_N = 0, + ref_size: Optional[int] = None, ): - """helper method to compute concrete size for a given axis and its reference axis. - If the reference axis is parameterized, `n` is used to compute the concrete size of it, see `ParameterizedSize`. + """Compute the concrete size for a given axis and its reference axis. + + Args: + axis: The axis this `SizeReference` is the size of. + ref_axis: The reference axis to compute the size from. + n: If the **ref_axis** is parameterized (of type `ParameterizedSize`) + and no fixed **ref_size** is given, + **n** is used to compute the size of the parameterized **ref_axis**. + ref_size: Overwrite the reference size instead of deriving it from + **ref_axis** + (**ref_axis.scale** is still used; any given **n** is ignored). """ assert ( axis.size == self @@ -367,22 +378,22 @@ def get_size( "`SizeReference` requires `axis` and `ref_axis` to have the same `unit`," f" but {axis.unit}!={ref_axis.unit}" ) - - if isinstance(ref_axis.size, (int, float)): - ref_size = ref_axis.size - elif isinstance(ref_axis.size, ParameterizedSize): - ref_size = ref_axis.size.get_size(n) - elif isinstance(ref_axis.size, DataDependentSize): - raise ValueError( - "Reference axis referenced in `SizeReference` may not be a `DataDependentSize`." - ) - elif isinstance(ref_axis.size, SizeReference): - raise ValueError( - "Reference axis referenced in `SizeReference` may not be sized by a" - + " `SizeReference` itself." - ) - else: - assert_never(ref_axis.size) + if ref_size is None: + if isinstance(ref_axis.size, (int, float)): + ref_size = ref_axis.size + elif isinstance(ref_axis.size, ParameterizedSize): + ref_size = ref_axis.size.get_size(n) + elif isinstance(ref_axis.size, DataDependentSize): + raise ValueError( + "Reference axis referenced in `SizeReference` may not be a `DataDependentSize`." + ) + elif isinstance(ref_axis.size, SizeReference): + raise ValueError( + "Reference axis referenced in `SizeReference` may not be sized by a" + + " `SizeReference` itself." + ) + else: + assert_never(ref_axis.size) return int(ref_size * ref_axis.scale / axis.scale + self.offset) @@ -2032,7 +2043,8 @@ class ModelDescr(GenericModelDescrBase, title="bioimage.io model specification") """Specialized resource type 'model'""" id: Optional[ModelId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" authors: NotEmpty[List[Author]] """The authors are the creators of the model RDF and the primary points of contact.""" @@ -2471,8 +2483,40 @@ def get_tensor_sizes( ) def get_axis_sizes( - self, ns: Mapping[Tuple[TensorId, AxisId], ParameterizedSize_N], batch_size: int + self, + ns: Mapping[Tuple[TensorId, AxisId], ParameterizedSize_N], + batch_size: Optional[int] = None, + *, + max_input_shape: Optional[Mapping[Tuple[TensorId, AxisId], int]] = None, ) -> _AxisSizes: + """Determine input and output block shape for scale factors **ns** + of parameterized input sizes. + + Args: + ns: Scale factor `n` for each axis (keyed by (tensor_id, axis_id)) + that is parameterized as `size = min + n * step`. + batch_size: The desired size of the batch dimension. + If given **batch_size** overwrites any batch size present in + **max_input_shape**. Default 1. + max_input_shape: Limits the derived block shapes. + Each axis for which the input size, parameterized by `n`, is larger + than **max_input_shape** is set to the minimal value `n_min` for which + this is still true. + Use this for small input samples or large values of **ns**. + Or simply whenever you know the full input shape. + + Returns: + Resolved axis sizes for model inputs and outputs. + """ + max_input_shape = max_input_shape or {} + if batch_size is None: + for (_t_id, a_id), s in max_input_shape.items(): + if a_id == BATCH_AXIS_ID: + batch_size = s + break + else: + batch_size = 1 + all_axes = { t.id: {a.id: a for a in t.axes} for t in chain(self.inputs, self.outputs) } @@ -2483,16 +2527,19 @@ def get_axis_sizes( def get_axis_size(a: Union[InputAxis, OutputAxis]): if isinstance(a, BatchAxis): if (t_descr.id, a.id) in ns: - raise ValueError( - "No size increment factor (n) for batch axis of tensor" - + f" '{t_descr.id}' expected." + logger.warning( + "Ignoring unexpected size increment factor (n) for batch axis" + + " of tensor '{}'.", + t_descr.id, ) return batch_size elif isinstance(a.size, int): if (t_descr.id, a.id) in ns: - raise ValueError( - "No size increment factor (n) for fixed size axis" - + f" '{a.id}' of tensor '{t_descr.id}' expected." + logger.warning( + "Ignoring unexpected size increment factor (n) for fixed size" + + " axis '{}' of tensor '{}'.", + a.id, + t_descr.id, ) return a.size elif isinstance(a.size, ParameterizedSize): @@ -2501,39 +2548,65 @@ def get_axis_size(a: Union[InputAxis, OutputAxis]): "Size increment factor (n) missing for parametrized axis" + f" '{a.id}' of tensor '{t_descr.id}'." ) - return a.size.get_size(ns[(t_descr.id, a.id)]) + n = ns[(t_descr.id, a.id)] + s_max = max_input_shape.get((t_descr.id, a.id)) + if s_max is not None: + n = min(n, a.size.get_n(s_max)) + + return a.size.get_size(n) + elif isinstance(a.size, SizeReference): if (t_descr.id, a.id) in ns: - raise ValueError( - f"No size increment factor (n) for axis '{a.id}' of tensor" - + f" '{t_descr.id}' with size reference expected." + logger.warning( + "Ignoring unexpected size increment factor (n) for axis '{}'" + + " of tensor '{}' with size reference.", + a.id, + t_descr.id, ) assert not isinstance(a, BatchAxis) ref_axis = all_axes[a.size.tensor_id][a.size.axis_id] assert not isinstance(ref_axis, BatchAxis) + ref_key = (a.size.tensor_id, a.size.axis_id) + ref_size = inputs.get(ref_key, outputs.get(ref_key)) + assert ref_size is not None, ref_key + assert not isinstance(ref_size, _DataDepSize), ref_key return a.size.get_size( axis=a, ref_axis=ref_axis, - n=ns.get((a.size.tensor_id, a.size.axis_id), 0), + ref_size=ref_size, ) elif isinstance(a.size, DataDependentSize): if (t_descr.id, a.id) in ns: - raise ValueError( - "No size increment factor (n) for data dependent size axis" - + f" '{a.id}' of tensor '{t_descr.id}' expected." + logger.warning( + "Ignoring unexpected increment factor (n) for data dependent" + + " size axis '{}' of tensor '{}'.", + a.id, + t_descr.id, ) return _DataDepSize(a.size.min, a.size.max) else: assert_never(a.size) + # first resolve all , but the `SizeReference` input sizes for t_descr in self.inputs: for a in t_descr.axes: - s = get_axis_size(a) - assert not isinstance(s, _DataDepSize) - inputs[t_descr.id, a.id] = s + if not isinstance(a.size, SizeReference): + s = get_axis_size(a) + assert not isinstance(s, _DataDepSize) + inputs[t_descr.id, a.id] = s + + # resolve all other input axis sizes + for t_descr in self.inputs: + for a in t_descr.axes: + if isinstance(a.size, SizeReference): + s = get_axis_size(a) + assert not isinstance(s, _DataDepSize) + inputs[t_descr.id, a.id] = s - for t_descr in chain(self.inputs, self.outputs): + # resolve all output axis sizes + for t_descr in self.outputs: for a in t_descr.axes: + assert not isinstance(a.size, ParameterizedSize) s = get_axis_size(a) outputs[t_descr.id, a.id] = s diff --git a/bioimageio/spec/notebook/v0_2.py b/bioimageio/spec/notebook/v0_2.py index a607a68cb..4d73c7426 100644 --- a/bioimageio/spec/notebook/v0_2.py +++ b/bioimageio/spec/notebook/v0_2.py @@ -40,7 +40,8 @@ class NotebookDescr(GenericDescrBase, title="bioimage.io notebook specification" type: Literal["notebook"] = "notebook" id: Optional[NotebookId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" source: NotebookSource """The Jupyter notebook""" diff --git a/bioimageio/spec/notebook/v0_3.py b/bioimageio/spec/notebook/v0_3.py index 48b5826a7..fabbf986d 100644 --- a/bioimageio/spec/notebook/v0_3.py +++ b/bioimageio/spec/notebook/v0_3.py @@ -32,7 +32,8 @@ class NotebookDescr(GenericDescrBase, title="bioimage.io notebook specification" type: Literal["notebook"] = "notebook" id: Optional[NotebookId] = None - """Model zoo (bioimage.io) wide, unique identifier (assigned by bioimage.io)""" + """bioimage.io-wide unique resource identifier + assigned by bioimage.io; version **un**specific.""" parent: Optional[NotebookId] = None """The description from which this one is derived""" diff --git a/dev/env.yaml b/dev/env.yaml index 689e5b766..0aacd6388 100644 --- a/dev/env.yaml +++ b/dev/env.yaml @@ -32,6 +32,7 @@ dependencies: - rich - ruff - ruyaml + - tifffile - tqdm - typing-extensions - zipp diff --git a/pyproject.toml b/pyproject.toml index a321abc91..f2c57611c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ exclude = [ "**/node_modules", "scripts/pdoc/original.py", "scripts/pdoc/patched.py", - "tests/cache", + "bioimageio_cache", "tests/old_*", ] include = ["bioimageio", "scripts", "tests"] diff --git a/setup.py b/setup.py index 4d6cce1cf..529d8f644 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ "requests", "rich", "ruyaml", + "tifffile", "tqdm", "typing-extensions", "zipp", diff --git a/tests/test_bioimageio_collection.py b/tests/test_bioimageio_collection.py index d7773ad53..4a824eb29 100644 --- a/tests/test_bioimageio_collection.py +++ b/tests/test_bioimageio_collection.py @@ -1,270 +1,106 @@ -import datetime import json from pathlib import Path -from typing import Any, Dict, Iterable, Mapping +from typing import Any, Collection, Dict, Iterable, Mapping, Tuple import pooch # pyright: ignore [reportMissingTypeStubs] import pytest from bioimageio.spec import settings -from bioimageio.spec._description import DISCOVER, LATEST -from bioimageio.spec._internal.types import FormatVersionPlaceholder -from tests.utils import ParameterSet, check_bioimageio_yaml +from bioimageio.spec.common import HttpUrl, Sha256 +from tests.utils import ParameterSet, check_bioimageio_yaml, skip_expensive -BASE_URL = "https://bioimage-io.github.io/collection-bioimage-io/" -RDF_BASE_URL = BASE_URL + "rdfs/" -WEEK = f"{datetime.datetime.now().year}week{datetime.datetime.now().isocalendar()[1]}" -CACHE_PATH = Path(__file__).parent / "cache" / WEEK +BASE_URL = "https://uk1s3.embassy.ebi.ac.uk/public-datasets/bioimage.io/" - -KNOWN_INVALID = { - "10.5281/zenodo.5749843/5888237/rdf.yaml", - "10.5281/zenodo.5910163/5942853/rdf.yaml", - "10.5281/zenodo.5910854/6539073/rdf.yaml", - "10.5281/zenodo.5914248/6514622/rdf.yaml", - "10.5281/zenodo.6559929/6559930/rdf.yaml", - "10.5281/zenodo.7614645/7642674/rdf.yaml", - "biapy/biapy/latest/rdf.yaml", - "biapy/notebook_classification_2d/latest/rdf.yaml", - "biapy/Notebook_semantic_segmentation_3d/latest/rdf.yaml", - "deepimagej/deepimagej/latest/rdf.yaml", - "deepimagej/DeepSTORMZeroCostDL4Mic/latest/rdf.yaml", - "deepimagej/Mt3VirtualStaining/latest/rdf.yaml", - "deepimagej/MU-Lux_CTC_PhC-C2DL-PSC/latest/rdf.yaml", - "deepimagej/SkinLesionClassification/latest/rdf.yaml", - "deepimagej/SMLMDensityMapEstimationDEFCoN/latest/rdf.yaml", - "deepimagej/UNet2DGlioblastomaSegmentation/latest/rdf.yaml", - "deepimagej/WidefieldDapiSuperResolution/latest/rdf.yaml", - "deepimagej/WidefieldFitcSuperResolution/latest/rdf.yaml", - "deepimagej/WidefieldTxredSuperResolution/latest/rdf.yaml", - "fiji/N2VSEMDemo/latest/rdf.yaml", - "ilastik/mitoem_segmentation_challenge/latest/rdf.yaml", - "imjoy/LuCa-7color/latest/rdf.yaml", - "zero/Dataset_CARE_2D_coli_DeepBacs/latest/rdf.yaml", - "zero/Dataset_fnet_DeepBacs/latest/rdf.yaml", - "zero/Dataset_Noise2Void_2D_subtilis_DeepBacs/latest/rdf.yaml", - "zero/Dataset_SplineDist_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_StarDist_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_U-Net_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_U-Net_2D_multilabel_DeepBacs/latest/rdf.yaml", - "zero/Dataset_YOLOv2_antibiotic_DeepBacs/latest/rdf.yaml", - "zero/Dataset_YOLOv2_coli_DeepBacs/latest/rdf.yaml", - "zero/Notebook_CycleGAN_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DecoNoising_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Detectron2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DRMIME_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_EmbedSeg_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_MaskRCNN_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_pix2pix_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_RetinaNet_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_StarDist_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_multilabel_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_3D_ZeroCostDL4Mic/latest/rdf.yaml", -} -KNOWN_INVALID_AS_LATEST = { - "10.5281/zenodo.5749843/5888237/rdf.yaml", - "10.5281/zenodo.5874841/6630266/rdf.yaml", - "10.5281/zenodo.5910163/5942853/rdf.yaml", - "10.5281/zenodo.5914248/6514622/rdf.yaml", - "10.5281/zenodo.5914248/8186255/rdf.yaml", - "10.5281/zenodo.6383429/7774505/rdf.yaml", - "10.5281/zenodo.6406803/6406804/rdf.yaml", - "10.5281/zenodo.6559474/6559475/rdf.yaml", - "10.5281/zenodo.6559929/6559930/rdf.yaml", - "10.5281/zenodo.6811491/6811492/rdf.yaml", - "10.5281/zenodo.6865412/6919253/rdf.yaml", - "10.5281/zenodo.7274275/8123818/rdf.yaml", - "10.5281/zenodo.7380171/7405349/rdf.yaml", - "10.5281/zenodo.7614645/7642674/rdf.yaml", - "10.5281/zenodo.8401064/8429203/rdf.yaml", - "10.5281/zenodo.8421755/8432366/rdf.yaml", - "biapy/biapy/latest/rdf.yaml", - "biapy/notebook_classification_2d/latest/rdf.yaml", - "biapy/notebook_classification_3d/latest/rdf.yaml", - "biapy/notebook_denoising_2d/latest/rdf.yaml", - "biapy/notebook_denoising_3d/latest/rdf.yaml", - "biapy/notebook_detection_2d/latest/rdf.yaml", - "biapy/notebook_detection_3d/latest/rdf.yaml", - "biapy/notebook_instance_segmentation_2d/latest/rdf.yaml", - "biapy/notebook_instance_segmentation_3d/latest/rdf.yaml", - "biapy/notebook_self_supervision_2d/latest/rdf.yaml", - "biapy/notebook_self_supervision_3d/latest/rdf.yaml", - "biapy/notebook_semantic_segmentation_2d/latest/rdf.yaml", - "biapy/Notebook_semantic_segmentation_3d/latest/rdf.yaml", - "biapy/notebook_super_resolution_2d/latest/rdf.yaml", - "biapy/notebook_super_resolution_3d/latest/rdf.yaml", - "bioimageio/stardist/latest/rdf.yaml", - "deepimagej/deepimagej-web/latest/rdf.yaml", - "deepimagej/deepimagej/latest/rdf.yaml", - "deepimagej/DeepSTORMZeroCostDL4Mic/latest/rdf.yaml", - "deepimagej/DeepSTORMZeroCostDL4Mic/latest/rdf.yaml", - "deepimagej/DeepSTORMZeroCostDL4Mic/latest/rdf.yaml", - "deepimagej/DeepSTORMZeroCostDL4Mic/latest/rdf.yaml", - "deepimagej/EVsTEMsegmentationFRUNet/latest/rdf.yaml", - "deepimagej/MoNuSeg_digital_pathology_miccai2018/latest/rdf.yaml", - "deepimagej/Mt3VirtualStaining/latest/rdf.yaml", - "deepimagej/MU-Lux_CTC_PhC-C2DL-PSC/latest/rdf.yaml", - "deepimagej/SkinLesionClassification/latest/rdf.yaml", - "deepimagej/smlm-deepimagej/latest/rdf.yaml", - "deepimagej/SMLMDensityMapEstimationDEFCoN/latest/rdf.yaml", - "deepimagej/unet-pancreaticcellsegmentation/latest/rdf.yaml", - "deepimagej/UNet2DGlioblastomaSegmentation/latest/rdf.yaml", - "deepimagej/WidefieldDapiSuperResolution/latest/rdf.yaml", - "deepimagej/WidefieldFitcSuperResolution/latest/rdf.yaml", - "deepimagej/WidefieldTxredSuperResolution/latest/rdf.yaml", - "dl4miceverywhere/DL4MicEverywhere/latest/rdf.yaml", - "dl4miceverywhere/Notebook_bioimageio_pytorch/latest/rdf.yaml", - "dl4miceverywhere/Notebook_bioimageio_tensorflow/latest/rdf.yaml", - "fiji/Fiji/latest/rdf.yaml", - "hpa/HPA-Classification/latest/rdf.yaml", - "hpa/hpa-kaggle-2021-dataset/latest/rdf.yaml", - "icy/icy/latest/rdf.yaml", - "ilastik/arabidopsis_tissue_atlas/latest/rdf.yaml", - "ilastik/cremi_training_data/latest/rdf.yaml", - "ilastik/ilastik/latest/rdf.yaml", - "ilastik/isbi2012_neuron_segmentation_challenge/latest/rdf.yaml", - "ilastik/mitoem_segmentation_challenge/latest/rdf.yaml", - "ilastik/mws-segmentation/latest/rdf.yaml", - "imjoy/BioImageIO-Packager/latest/rdf.yaml", - "imjoy/GenericBioEngineApp/latest/rdf.yaml", - "imjoy/HPA-Single-Cell/latest/rdf.yaml", - "imjoy/ImageJ.JS/latest/rdf.yaml", - "imjoy/ImJoy/latest/rdf.yaml", - "imjoy/LuCa-7color/latest/rdf.yaml", - "imjoy/vizarr/latest/rdf.yaml", - "qupath/QuPath/latest/rdf.yaml", - "stardist/stardist/latest/rdf.yaml", - "zero/Dataset_CARE_2D_coli_DeepBacs/latest/rdf.yaml", - "zero/Dataset_CARE_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_CARE_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_CycleGAN_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_Deep-STORM_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_fnet_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_fnet_DeepBacs/latest/rdf.yaml", - "zero/Dataset_Noise2Void_2D_subtilis_DeepBacs/latest/rdf.yaml", - "zero/Dataset_Noise2Void_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_Noise2Void_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_Noisy_Nuclei_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_pix2pix_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_SplineDist_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_StarDist_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_StarDist_2D_ZeroCostDL4Mic_2D/latest/rdf.yaml", - "zero/Dataset_StarDist_brightfield_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_StarDist_brightfield2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_StarDist_Fluo_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_StarDist_fluo2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Dataset_U-Net_2D_DeepBacs/latest/rdf.yaml", - "zero/Dataset_U-Net_2D_multilabel_DeepBacs/latest/rdf.yaml", - "zero/Dataset_YOLOv2_antibiotic_DeepBacs/latest/rdf.yaml", - "zero/Dataset_YOLOv2_coli_DeepBacs/latest/rdf.yaml", - "zero/Dataset_YOLOv2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook Preview/latest/rdf.yaml", - "zero/Notebook_Augmentor_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_CARE_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_CARE_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Cellpose_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_CycleGAN_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_CycleGAN_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DecoNoising_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DecoNoising_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Deep-STORM_2D_ZeroCostDL4Mic_DeepImageJ/latest/rdf.yaml", - "zero/Notebook_Deep-STORM_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DenoiSeg_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Detectron2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Detectron2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DFCAN_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DRMIME_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_DRMIME_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_EmbedSeg_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_EmbedSeg_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_fnet_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_fnet_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Interactive_Segmentation_Kaibu_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_MaskRCNN_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_MaskRCNN_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Noise2Void_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_Noise2Void_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_pix2pix_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_pix2pix_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/notebook_preview/latest/rdf.yaml-latest", - "zero/notebook_preview/latest/rdf.yaml", - "zero/Notebook_Quality_Control_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_RCAN_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_RetinaNet_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_RetinaNet_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_SplineDist_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_StarDist_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_StarDist_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_StarDist_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_multilabel_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_multilabel_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_2D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_U-Net_3D_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/Notebook_YOLOv2_ZeroCostDL4Mic/latest/rdf.yaml", - "zero/WGAN_ZeroCostDL4Mic.ipynb/latest/rdf.yaml", +KNOWN_INVALID: Collection[str] = set() +EXCLUDE_FIELDS_FROM_ROUNDTRIP_DEFAULT: Collection[str] = { + "version_number", # deprecated field that gets dropped in favor of `version`` + "version", # may be set from deprecated `version_number` } -EXCLUDE_FIELDS_FROM_ROUNDTRIP = { - "10.5281/zenodo.6348728/6348729/rdf.yaml": {"cite"}, # doi prefixed - "10.5281/zenodo.6406803/6406804/rdf.yaml": {"cite"}, # doi prefixed - "10.5281/zenodo.6338614/6338615/rdf.yaml": {"cite"}, # doi prefixed - "10.5281/zenodo.5914248/8186255/rdf.yaml": {"cite"}, # doi prefixed - "10.5281/zenodo.7274275/8123818/rdf.yaml": {"inputs", "parent"}, - "10.5281/zenodo.7315440/7315441/rdf.yaml": { - "cite", - "maintainers", - "weights", - }, # weights.onnx: missing sh256, cite[0].doi: prefix - "10.5281/zenodo.7772662/7781091/rdf.yaml": { - "weights" - }, # upper to lower case sha256 - "10.5281/zenodo.6028097/6028098/rdf.yaml": { - "authors", # gh username "Constantin Pape" -> contantinpape - "maintainers", - }, - "zero/Notebook Preview/latest/rdf.yaml": {"rdf_source"}, # ' ' -> %20 +EXCLUDE_FIELDS_FROM_ROUNDTRIP: Mapping[str, Collection[str]] = { + "affable-shark/1.1": {"inputs"}, # preprocessing assert_dtype added + "philosophical-panda/0.0.11": {"outputs"}, # int -> float + "philosophical-panda/0.1.0": {"outputs"}, # int -> float + "dynamic-t-rex/1": {"inputs"}, # int -> float + "charismatic-whale/1.0.1": {"inputs", "outputs"}, # int -> float + "impartial-shrimp/1.1": {"inputs"}, # preprocessing assert_dtype added } -def yield_bioimageio_yaml_urls() -> Iterable[ParameterSet]: - collection_path: Any = pooch.retrieve( - BASE_URL + "collection.json", None, path=settings.cache_path +def _get_rdf_sources(): + all_versions_path: Any = pooch.retrieve( + BASE_URL + "all_versions.json", None, path=settings.cache_path ) - with Path(collection_path).open(encoding="utf-8") as f: - collection_data = json.load(f)["collection"] + with Path(all_versions_path).open(encoding="utf-8") as f: + entries = json.load(f)["entries"] + + ret: Dict[str, Tuple[HttpUrl, Sha256]] = {} + for entry in entries: + for version in entry["versions"]: + ret[f"{entry['concept']}/{version['v']}"] = ( + HttpUrl(version["source"]), + Sha256(version["sha256"]), + ) - collection_registry: Dict[str, None] = { - entry["rdf_source"].replace(RDF_BASE_URL, ""): None for entry in collection_data - } + return ret - for rdf in collection_registry: - descr_url = RDF_BASE_URL + rdf - key = rdf - yield pytest.param(descr_url, key, id=key) +ALL_RDF_SOURCES: Mapping[str, Tuple[HttpUrl, Sha256]] = _get_rdf_sources() + + +def yield_bioimageio_yaml_urls() -> Iterable[ParameterSet]: + for descr_url, sha in ALL_RDF_SOURCES.values(): + key = ( + descr_url.replace(BASE_URL, "") + .replace("/files/rdf.yaml", "") + .replace("/files/bioimageio.yaml", "") + ) + yield pytest.param(descr_url, sha, key, id=key) -@pytest.mark.parametrize("format_version", [DISCOVER, LATEST]) -@pytest.mark.parametrize("descr_url,key", list(yield_bioimageio_yaml_urls())) + +@skip_expensive +@pytest.mark.parametrize("descr_url,sha,key", list(yield_bioimageio_yaml_urls())) def test_rdf( descr_url: Path, + sha: Sha256, key: str, - format_version: FormatVersionPlaceholder, bioimageio_json_schema: Mapping[Any, Any], ): - if ( - format_version == DISCOVER - and key in KNOWN_INVALID - or format_version == LATEST - and key in KNOWN_INVALID_AS_LATEST - ): + if key in KNOWN_INVALID: pytest.skip("known failure") check_bioimageio_yaml( descr_url, - as_latest=format_version == LATEST, - exclude_fields_from_roundtrip=EXCLUDE_FIELDS_FROM_ROUNDTRIP.get(key, set()), + sha=sha, + as_latest=False, + exclude_fields_from_roundtrip=EXCLUDE_FIELDS_FROM_ROUNDTRIP.get( + key, EXCLUDE_FIELDS_FROM_ROUNDTRIP_DEFAULT + ), bioimageio_json_schema=bioimageio_json_schema, perform_io_checks=False, ) + + +@skip_expensive +@pytest.mark.parametrize( + "rdf_id", + [ + "10.5281/zenodo.5764892/1.1", # affable-shark/1.1 + "ambitious-sloth/1.2", + "breezy-handbag/1", + "ilastik/ilastik/1", + "uplifting-ice-cream/1", + ], +) +def test_exemplary_rdf(rdf_id: str, bioimageio_json_schema: Mapping[Any, Any]): + """test a list of models we expect to be compatible with the latest spec version""" + source, sha = ALL_RDF_SOURCES[rdf_id] + check_bioimageio_yaml( + source, + sha=sha, + as_latest=True, + exclude_fields_from_roundtrip=EXCLUDE_FIELDS_FROM_ROUNDTRIP.get( + rdf_id, EXCLUDE_FIELDS_FROM_ROUNDTRIP_DEFAULT + ), + bioimageio_json_schema=bioimageio_json_schema, + perform_io_checks=True, + ) diff --git a/tests/test_model/test_v0_5.py b/tests/test_model/test_v0_5.py index 569f64fb4..643107383 100644 --- a/tests/test_model/test_v0_5.py +++ b/tests/test_model/test_v0_5.py @@ -1,10 +1,12 @@ +from copy import deepcopy from datetime import datetime -from typing import Any, Dict, Union +from types import MappingProxyType +from typing import Any, Dict, Mapping, Union import pytest from pydantic import RootModel, ValidationError -from bioimageio.spec import validate_format +from bioimageio.spec import build_description, validate_format from bioimageio.spec._internal.io import FileDescr from bioimageio.spec._internal.license_id import LicenseId from bioimageio.spec._internal.url import HttpUrl @@ -24,6 +26,8 @@ ModelDescr, OnnxWeightsDescr, OutputTensorDescr, + ParameterizedSize, + SizeReference, SpaceInputAxis, SpaceOutputAxis, TensorDescrBase, @@ -177,9 +181,6 @@ def test_input_tensor_invalid(kwargs: Dict[str, Any]): ) -@pytest.mark.skip( - "possibly bug in pydantic? in some envs it passes, in ohters not" -) # TODO: fix def test_input_tensor_error_count(model_data: Dict[str, Any]): """this test checks that the discrminated union for `InputAxis` does its thing and we don't get errors for all options""" @@ -223,10 +224,12 @@ def test_input_axis(kwargs: Union[Dict[str, Any], SpaceInputAxis]): check_type(InputAxis, kwargs) -@pytest.fixture -def model_data(): +@pytest.fixture(scope="module") +def model(): + """reuse model object to avoid expensive model validation, + use only when not manipulating the model!""" with ValidationContext(perform_io_checks=False): - model = ModelDescr( + return ModelDescr( documentation=UNET2D_ROOT / "README.md", license=LicenseId("MIT"), git_repo=HttpUrl("https://github.com/bioimage-io/core-bioimage-io-python"), @@ -254,8 +257,15 @@ def model_data(): axes=[ BatchAxis(), ChannelAxis(channel_names=[Identifier("intensity")]), - SpaceInputAxis(id=AxisId("x"), size=512), - SpaceInputAxis(id=AxisId("y"), size=512), + SpaceInputAxis( + id=AxisId("x"), + size=SizeReference( + tensor_id=TensorId("input_1"), axis_id=AxisId("y") + ), + ), + SpaceInputAxis( + id=AxisId("y"), size=ParameterizedSize(min=256, step=8) + ), ], test_tensor=FileDescr(source=UNET2D_ROOT / "test_input.npy"), ), @@ -282,12 +292,21 @@ def model_data(): ), type="model", ) - data = model.model_dump(mode="json") - assert data["documentation"] == str(UNET2D_ROOT / "README.md"), ( - data["documentation"], - str(UNET2D_ROOT / "README.md"), - ) - return data + + +@pytest.fixture(scope="module") +def const_model_data(model: ModelDescr): + data = model.model_dump(mode="json") + assert data["documentation"] == str(UNET2D_ROOT / "README.md"), ( + data["documentation"], + str(UNET2D_ROOT / "README.md"), + ) + return MappingProxyType(data) + + +@pytest.fixture +def model_data(const_model_data: Mapping[str, Any]): + return deepcopy(dict(const_model_data)) @pytest.mark.parametrize( @@ -383,17 +402,17 @@ def test_output_fixed_shape_too_small(model_data: Dict[str, Any]): assert summary.status == "failed", summary.format() -def test_get_axis_sizes_raises_with_surplus_n(model_data: Dict[str, Any]): - with ValidationContext(perform_io_checks=False): - model = ModelDescr(**model_data) +def test_get_axis_sizes_with_surplus_n(model: ModelDescr): + key = (model.inputs[0].id, AxisId("y")) + _ = model.get_axis_sizes(ns={key: 1}, batch_size=1) - output_tensor_id = model.inputs[0].id - output_axis_id = AxisId("y") - with pytest.raises(ValueError): - _ = model.get_axis_sizes( - ns={(output_tensor_id, output_axis_id): 1}, batch_size=1 - ) +def test_get_axis_sizes_with_partial_max_size(model: ModelDescr): + key = (model.inputs[0].id, AxisId("y")) + ns = {key: 100} + wo_max_shape = model.get_axis_sizes(ns=ns) + with_max_shape = model.get_axis_sizes(ns=ns, max_input_shape={key: 32}) + assert wo_max_shape.inputs[key] > with_max_shape.inputs[key] def test_get_axis_sizes_raises_with_missing_n(model_data: Dict[str, Any]): @@ -414,7 +433,7 @@ def test_output_ref_shape_mismatch(model_data: Dict[str, Any]): model_data["outputs"][0]["axes"][2] = { "type": "space", "id": "x", - "size": {"tensor_id": "input_1", "axis_id": "x"}, + "size": {"tensor_id": "input_1", "axis_id": "y"}, "halo": 2, } summary = validate_format( @@ -438,7 +457,7 @@ def test_output_ref_shape_too_small(model_data: Dict[str, Any]): model_data["outputs"][0]["axes"][2] = { "type": "space", "id": "x", - "size": {"tensor_id": "input_1", "axis_id": "x"}, + "size": {"tensor_id": "input_1", "axis_id": "y"}, "halo": 2, } summary = validate_format( @@ -463,16 +482,45 @@ def test_model_has_parent_with_id(model_data: Dict[str, Any]): def test_model_with_expanded_output(model_data: Dict[str, Any]): model_data["outputs"][0]["axes"] = [ - {"type": "space", "id": "x", "size": {"tensor_id": "input_1", "axis_id": "x"}}, - {"type": "space", "id": "y", "size": {"tensor_id": "input_1", "axis_id": "y"}}, + { + "type": "space", + "id": "x", + "size": {"tensor_id": "input_1", "axis_id": "y"}, + "scale": 0.5, + }, + { + "type": "space", + "id": "y", + "size": {"tensor_id": "input_1", "axis_id": "y"}, + "scale": 4, + }, {"type": "space", "id": "z", "size": 7}, {"type": "channel", "channel_names": list("abc")}, + {"type": "batch"}, ] - summary = validate_format( + model = build_description( model_data, context=ValidationContext(perform_io_checks=False) ) - assert summary.status == "passed", summary.format() + assert isinstance(model, ModelDescr), model.validation_summary.format() + actual_inputs, actual_outputs = model.get_axis_sizes( + {(TensorId("input_1"), AxisId("y")): 1}, batch_size=13 + ) + expected_inputs = { + (TensorId("input_1"), AxisId("batch")): 13, + (TensorId("input_1"), AxisId("channel")): 1, + (TensorId("input_1"), AxisId("x")): 256 + 8, + (TensorId("input_1"), AxisId("y")): 256 + 8, + } + expected_outputs = { + (TensorId("output_1"), AxisId("batch")): 13, + (TensorId("output_1"), AxisId("channel")): 3, + (TensorId("output_1"), AxisId("x")): 2 * (256 + 8), + (TensorId("output_1"), AxisId("y")): (256 + 8) // 4, + (TensorId("output_1"), AxisId("z")): 7, + } + assert actual_inputs == expected_inputs + assert actual_outputs == expected_outputs def test_model_rdf_is_valid_general_rdf(model_data: Dict[str, Any]): @@ -505,3 +553,16 @@ def test_empty_axis_data(): with pytest.raises(ValidationError): _ = OutputAxis.model_validate({}) + + +@pytest.mark.parametrize( + "a,b", [(TensorId("t"), TensorId("t")), (AxisId("a"), AxisId("a"))] +) +def test_identifier_identity(a: Any, b: Any): + assert a == b + + +def test_validate_parameterized_size(model: ModelDescr): + param_size = model.inputs[0].axes[3].size + assert isinstance(param_size, ParameterizedSize), type(param_size) + assert (actual := param_size.validate_size(512)) == 512, actual diff --git a/tests/utils.py b/tests/utils.py index d714000c1..14fd71e85 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,16 +1,17 @@ +import os from contextlib import nullcontext from copy import deepcopy from io import StringIO from pathlib import Path from typing import ( Any, + Collection, ContextManager, Dict, Mapping, Optional, Protocol, Sequence, - Set, Type, Union, ) @@ -28,13 +29,12 @@ ) from ruyaml import YAML -from bioimageio.spec._description import InvalidDescr, build_description +from bioimageio.spec import InvalidDescr, ValidationContext, build_description from bioimageio.spec._internal.common_nodes import Node from bioimageio.spec._internal.io import download from bioimageio.spec._internal.root_url import RootHttpUrl -from bioimageio.spec._internal.url import HttpUrl -from bioimageio.spec._internal.validation_context import ValidationContext from bioimageio.spec.application.v0_2 import ApplicationDescr as ApplicationDescr02 +from bioimageio.spec.common import HttpUrl, Sha256 from bioimageio.spec.dataset.v0_2 import DatasetDescr as DatasetDescr02 from bioimageio.spec.generic._v0_2_converter import DOI_PREFIXES from bioimageio.spec.generic.v0_2 import GenericDescr as GenericDescr02 @@ -46,6 +46,11 @@ unset = object() +skip_expensive = pytest.mark.skipif( + (run := os.getenv("SKIP_EXPENSIVE_TESTS")) == "true", + reason=f"Skipping expensive test (SKIP_EXPENSIVE_TESTS {run}!=true )", +) + def check_node( node_class: Type[Node], @@ -137,14 +142,15 @@ def check_bioimageio_yaml( source: Union[Path, HttpUrl], /, *, + sha: Optional[Sha256] = None, root: Union[RootHttpUrl, DirectoryPath, ZipFile] = Path(), as_latest: bool, - exclude_fields_from_roundtrip: Set[str] = set(), + exclude_fields_from_roundtrip: Collection[str] = set(), is_invalid: bool = False, bioimageio_json_schema: Optional[Mapping[Any, Any]], perform_io_checks: bool = True, ) -> None: - downloaded_source = download(source) + downloaded_source = download(source, sha256=sha) root = downloaded_source.original_root raw = downloaded_source.path.read_text(encoding="utf-8") assert isinstance(raw, str)