diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 76770f73..05ad1f92 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,22 +14,26 @@ on: jobs: test: runs-on: ubuntu-latest + env: + UV_NO_SYNC: "1" strategy: matrix: - python-version: - - "3.11" + python-version: ["3.11"] + with-zarr: [true, false] + resolution: [highest, lowest-direct] steps: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Set up Python ${{ matrix.python-version }} - run: uv python install ${{ matrix.python-version }} + uses: astral-sh/setup-uv@v6 + with: + python-version: ${{ matrix.python-version }} - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --no-dev --group test ${{ matrix.with-zarr && '--extra zarr' || '' }} + env: + UV_RESOLUTION: ${{ matrix.resolution }} - name: Run tests # For example, using `pytest` diff --git a/.readthedocs.yml b/.readthedocs.yml index ff6c14c0..6e43d10f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,14 +4,15 @@ build: os: ubuntu-24.04 tools: python: "3" - -python: - install: - - method: pip - path: . - extra_requirements: - - docs - - pydantic + jobs: + pre_create_environment: + - asdf plugin add uv + - asdf install uv latest + - asdf global uv latest + create_environment: + - uv venv "${READTHEDOCS_VIRTUALENV_PATH}" + install: + - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --group docs mkdocs: configuration: mkdocs.yml diff --git a/docs/index.md b/docs/index.md index cf588b68..c0e2fc25 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,8 @@ Each class has ```sh pip install ome-zarr-models +# or ... with zarr io support +pip install ome-zarr-models[zarr] ``` or diff --git a/docs/tutorial.py b/docs/tutorial.py index 24840fac..0419a1eb 100644 --- a/docs/tutorial.py +++ b/docs/tutorial.py @@ -89,6 +89,7 @@ fill_value=0, codecs=[NamedConfig(name="bytes")], dimension_names=["y", "x"], + attributes={}, ), ArraySpec( shape=(100, 100), @@ -103,6 +104,7 @@ fill_value=0, codecs=[NamedConfig(name="bytes")], dimension_names=["y", "x"], + attributes={}, ), ] diff --git a/pyproject.toml b/pyproject.toml index 74392848..d831c827 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] description = "A minimal Python package for reading OME-Zarr (meta)data " readme = "README.md" requires-python = ">=3.11" -dependencies = ["zarr>= 3.1.1", "pydantic >= 2.11.5", "pydantic-zarr >= 0.8.2"] +dependencies = ["pydantic >= 2.11.5", "pydantic-zarr>=0.8.2"] maintainers = [{ name = "David Stansby" }] license = { file = "LICENSE" } classifiers = [ @@ -21,16 +21,8 @@ Repository = "https://github.com/ome-zarr-models/ome-zarr-models-py" # Changelog = "" [project.optional-dependencies] -docs = [ - "mkdocs>=1.6.1", - "mkdocstrings-python>=1.12.2", - "mkdocs-material", - "mkdocs-jupyter", - "matplotlib", - "rich", - "griffe-pydantic", - "fsspec[http]", -] +zarr = ["zarr>= 3.1.1"] + [project.scripts] ome-zarr-models = "ome_zarr_models._cli:main" @@ -43,12 +35,30 @@ requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" [dependency-groups] -docs = ["ome-zarr-models[docs]"] -dev = ["mypy", "ruff>=0.8", "pre-commit"] -test = ["pytest>=8.3.3", "pytest-cov", "pytest-recording"] +docs = [ + "mkdocs>=1.6.1", + "mkdocstrings-python>=1.12.2", + "mkdocs-material", + "mkdocs-jupyter >=0.25", + "matplotlib >=3.10", + "rich >= 14", + "griffe-pydantic", + "fsspec[http] >=2025", +] +dev = [ + { include-group = "docs" }, + { include-group = "test" }, + "mypy >=1.18", + "ruff>=0.8", + "pre-commit", + "ome-zarr-models[zarr]", +] +test = ["pytest>=8.3.3", "pytest-cov >=7.0.0", "pytest-recording", "fsspec[http] >=2025", "typos>=0.6.0"] + -[tool.uv] -default-groups = ["docs", "dev", "test"] +# TEMPORARY: remove before merge +[tool.uv.sources] +pydantic-zarr = { git = "https://github.com/zarr-developers/pydantic-zarr", rev = "main" } # Ruff configuration for linting and formatting # https://docs.astral.sh/ruff diff --git a/src/ome_zarr_models/__init__.py b/src/ome_zarr_models/__init__.py index 89bf128c..2bcde7dd 100644 --- a/src/ome_zarr_models/__init__.py +++ b/src/ome_zarr_models/__init__.py @@ -1,9 +1,8 @@ +from __future__ import annotations + from importlib.metadata import PackageNotFoundError, version from typing import TYPE_CHECKING, Any, Literal -import zarr -import zarr.storage - import ome_zarr_models.v04.hcs import ome_zarr_models.v04.image import ome_zarr_models.v04.image_label @@ -18,6 +17,10 @@ from ome_zarr_models.v04.base import BaseGroupv04 from ome_zarr_models.v05.base import BaseGroupv05 +if TYPE_CHECKING: + import zarr + import zarr.storage + try: __version__ = version("ome_zarr_models") except PackageNotFoundError: # pragma: no cover @@ -99,6 +102,14 @@ def open_ome_zarr( take a long time. It will be quicker to directly use the OME-Zarr group class if you know which version and group you expect. """ + try: + import zarr + except ImportError as e: + raise ImportError( + "zarr must be installed to use open_ome_zarr(). " + "You can install it with 'pip install ome-zarr-models[zarr]'" + ) from e + if not isinstance(group, zarr.Group): zarr_format = _ome_zarr_zarr_map.get(version, None) # type: ignore[arg-type] group = zarr.open_group(group, zarr_format=zarr_format, mode="r") diff --git a/src/ome_zarr_models/_utils.py b/src/ome_zarr_models/_utils.py index 4a2966e8..5f99cb7c 100644 --- a/src/ome_zarr_models/_utils.py +++ b/src/ome_zarr_models/_utils.py @@ -13,6 +13,11 @@ import pydantic_zarr.v3 from pydantic import create_model +if TYPE_CHECKING: + from collections.abc import Iterable + + from zarr.abc.store import Store + from ome_zarr_models.base import BaseAttrsv2, BaseAttrsv3 from ome_zarr_models.common.validation import ( check_array_path, diff --git a/src/ome_zarr_models/_v06/hcs.py b/src/ome_zarr_models/_v06/hcs.py index 78c9e80c..0fc8d0d5 100644 --- a/src/ome_zarr_models/_v06/hcs.py +++ b/src/ome_zarr_models/_v06/hcs.py @@ -1,9 +1,10 @@ +from __future__ import annotations + from collections.abc import Generator, Mapping from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr from pydantic import model_validator from pydantic_zarr.v3 import GroupSpec @@ -14,6 +15,7 @@ from ome_zarr_models.common.well import WellGroupNotFoundError if TYPE_CHECKING: + import zarr from pydantic_zarr.v3 import AnyGroupSpec __all__ = ["HCS", "HCSAttrs"] diff --git a/src/ome_zarr_models/_v06/image.py b/src/ome_zarr_models/_v06/image.py index 7a60ae27..38873762 100644 --- a/src/ome_zarr_models/_v06/image.py +++ b/src/ome_zarr_models/_v06/image.py @@ -1,10 +1,10 @@ +from __future__ import annotations + from collections.abc import Sequence -from typing import Self +from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr -import zarr.errors from pydantic import Field, JsonValue, model_validator from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec @@ -15,6 +15,12 @@ from ome_zarr_models._v06.multiscales import Dataset, Multiscale from ome_zarr_models.common.coordinate_transformations import _build_transforms +if TYPE_CHECKING: + from collections.abc import Sequence + + import zarr + + __all__ = ["Image", "ImageAttrs"] @@ -71,7 +77,7 @@ def new( metadata: JsonValue | None = None, global_scale: Sequence[float] | None = None, global_translation: Sequence[float] | None = None, - ) -> "Image": + ) -> Image: """ Create a new `Image` from a sequence of multiscale arrays and spatial metadata. diff --git a/src/ome_zarr_models/_v06/image_label.py b/src/ome_zarr_models/_v06/image_label.py index d017157c..16b8ed8b 100644 --- a/src/ome_zarr_models/_v06/image_label.py +++ b/src/ome_zarr_models/_v06/image_label.py @@ -1,8 +1,9 @@ -from typing import Self +from __future__ import annotations + +from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr from pydantic import Field from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs @@ -10,6 +11,10 @@ from ome_zarr_models._v06.image_label_types import Label from ome_zarr_models._v06.multiscales import Multiscale +if TYPE_CHECKING: + import zarr + + __all__ = ["ImageLabel", "ImageLabelAttrs"] diff --git a/src/ome_zarr_models/_v06/labels.py b/src/ome_zarr_models/_v06/labels.py index b19995e7..88ca209a 100644 --- a/src/ome_zarr_models/_v06/labels.py +++ b/src/ome_zarr_models/_v06/labels.py @@ -1,11 +1,11 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Self import numpy as np # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr -import zarr.errors from pydantic import Field, ValidationError, model_validator from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec @@ -13,6 +13,8 @@ from ome_zarr_models.common.validation import check_array_spec, check_group_spec if TYPE_CHECKING: + import zarr + from ome_zarr_models._v06.image_label import ImageLabel @@ -34,7 +36,7 @@ ] -def _check_valid_dtypes(labels: "Labels") -> "Labels": +def _check_valid_dtypes(labels: Labels) -> Labels: """ Check that all multiscales levels of a labels image are valid Label data types. """ @@ -102,6 +104,12 @@ def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override] group : zarr.Group A Zarr group that has valid OME-Zarr label metadata. """ + try: + import zarr + import zarr.errors + except ImportError as e: + raise ImportError("zarr is required to use this function") from e + from ome_zarr_models._v06.image_label import ImageLabel attrs_dict = group.attrs.asdict() @@ -141,7 +149,7 @@ def label_paths(self) -> list[str]: """ return self.attributes.ome.labels - def get_image_labels_group(self, path: str) -> "ImageLabel": + def get_image_labels_group(self, path: str) -> ImageLabel: """ Get a image labels group at a given path. """ diff --git a/src/ome_zarr_models/_v06/well.py b/src/ome_zarr_models/_v06/well.py index 4de9b122..c33e5a64 100644 --- a/src/ome_zarr_models/_v06/well.py +++ b/src/ome_zarr_models/_v06/well.py @@ -1,13 +1,17 @@ -# Import needed for pydantic type resolution -from typing import Self +from __future__ import annotations -import zarr +# Import needed for pydantic type resolution +from typing import TYPE_CHECKING, Self from ome_zarr_models._utils import _from_zarr_v3 from ome_zarr_models._v06.base import BaseGroupv06, BaseOMEAttrs from ome_zarr_models._v06.image import Image from ome_zarr_models._v06.well_types import WellMeta +if TYPE_CHECKING: + import zarr + + __all__ = ["Well", "WellAttrs"] diff --git a/src/ome_zarr_models/common/validation.py b/src/ome_zarr_models/common/validation.py index 17a4ba1f..5c31570c 100644 --- a/src/ome_zarr_models/common/validation.py +++ b/src/ome_zarr_models/common/validation.py @@ -3,8 +3,6 @@ from typing import TYPE_CHECKING, Literal, TypeVar, overload -import zarr -import zarr.errors from pydantic import StringConstraints from pydantic_zarr.v2 import AnyArraySpec as AnyArraySpecv2 from pydantic_zarr.v2 import AnyGroupSpec as AnyGroupSpecv2 @@ -20,6 +18,8 @@ if TYPE_CHECKING: from collections.abc import Sequence + import zarr + __all__ = [ "AlphaNumericConstraint", @@ -89,6 +89,12 @@ def check_array_path( ValueError If the array doesn't exist, or the array is not the expected Zarr version. """ + try: + import zarr + import zarr.errors + except ImportError as e: + raise ImportError("zarr is required to use this function") from e + try: array = zarr.open_array( store=group.store_path, @@ -158,6 +164,9 @@ def check_group_path( ValueError If the group doesn't exist, or the group is not the expected Zarr version. """ + import zarr + import zarr.errors + try: group = zarr.open_group( store=group.store_path, diff --git a/src/ome_zarr_models/v04/base.py b/src/ome_zarr_models/v04/base.py index fb88a7fc..abea2987 100644 --- a/src/ome_zarr_models/v04/base.py +++ b/src/ome_zarr_models/v04/base.py @@ -1,10 +1,15 @@ -from typing import Generic, Literal, Self, TypeVar +from __future__ import annotations + +from typing import TYPE_CHECKING, Generic, Literal, Self, TypeVar -import zarr from pydantic_zarr.v2 import GroupSpec, TBaseItem from ome_zarr_models.base import BaseAttrsv2, BaseGroup +if TYPE_CHECKING: + import zarr + + T = TypeVar("T", bound=BaseAttrsv2) diff --git a/src/ome_zarr_models/v04/hcs.py b/src/ome_zarr_models/v04/hcs.py index af7b2dae..8ca3334a 100644 --- a/src/ome_zarr_models/v04/hcs.py +++ b/src/ome_zarr_models/v04/hcs.py @@ -1,7 +1,8 @@ +from __future__ import annotations + from collections.abc import Generator, Mapping -from typing import Self +from typing import TYPE_CHECKING, Self -import zarr from pydantic import model_validator from pydantic_zarr.v2 import AnyGroupSpec, GroupSpec @@ -12,6 +13,9 @@ from ome_zarr_models.v04.plate import Plate from ome_zarr_models.v04.well import Well +if TYPE_CHECKING: + import zarr + __all__ = ["HCS", "HCSAttrs"] diff --git a/src/ome_zarr_models/v04/image_label.py b/src/ome_zarr_models/v04/image_label.py index ee2f32a6..e772d8a3 100644 --- a/src/ome_zarr_models/v04/image_label.py +++ b/src/ome_zarr_models/v04/image_label.py @@ -1,6 +1,7 @@ -from typing import Self +from __future__ import annotations + +from typing import TYPE_CHECKING, Self -import zarr from pydantic import Field from ome_zarr_models.base import BaseAttrsv2 @@ -9,6 +10,9 @@ from ome_zarr_models.v04.image_label_types import Label from ome_zarr_models.v04.multiscales import Multiscale +if TYPE_CHECKING: + import zarr + __all__ = ["ImageLabel", "ImageLabelAttrs"] diff --git a/src/ome_zarr_models/v04/well.py b/src/ome_zarr_models/v04/well.py index dacbfb7a..2ef2a935 100644 --- a/src/ome_zarr_models/v04/well.py +++ b/src/ome_zarr_models/v04/well.py @@ -2,11 +2,9 @@ For reference, see the [well section of the OME-Zarr specification](https://ngff.openmicroscopy.org/0.4/#well-md). """ -from collections.abc import Generator -from typing import Self +from __future__ import annotations -import zarr -from pydantic_zarr.v2 import AnyGroupSpec +from typing import TYPE_CHECKING, Self from ome_zarr_models._utils import _from_zarr_v2 from ome_zarr_models.base import BaseAttrsv2 @@ -14,6 +12,12 @@ from ome_zarr_models.v04.image import Image from ome_zarr_models.v04.well_types import WellMeta +if TYPE_CHECKING: + from collections.abc import Generator + + import zarr + from pydantic_zarr.v2 import AnyGroupSpec + __all__ = ["Well", "WellAttrs"] diff --git a/src/ome_zarr_models/v05/hcs.py b/src/ome_zarr_models/v05/hcs.py index 858ecb74..3285171e 100644 --- a/src/ome_zarr_models/v05/hcs.py +++ b/src/ome_zarr_models/v05/hcs.py @@ -1,9 +1,10 @@ +from __future__ import annotations + from collections.abc import Generator, Mapping from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr from pydantic import model_validator from pydantic_zarr.v3 import GroupSpec @@ -14,6 +15,7 @@ from ome_zarr_models.v05.well import Well if TYPE_CHECKING: + import zarr from pydantic_zarr.v3 import AnyGroupSpec diff --git a/src/ome_zarr_models/v05/image.py b/src/ome_zarr_models/v05/image.py index a945ef6f..9209303f 100644 --- a/src/ome_zarr_models/v05/image.py +++ b/src/ome_zarr_models/v05/image.py @@ -1,10 +1,10 @@ +from __future__ import annotations + from collections.abc import Sequence -from typing import Self +from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr -import zarr.errors from pydantic import Field, JsonValue, model_validator from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec @@ -15,6 +15,11 @@ from ome_zarr_models.v05.labels import Labels from ome_zarr_models.v05.multiscales import Dataset, Multiscale +if TYPE_CHECKING: + from collections.abc import Sequence + + import zarr + __all__ = ["Image", "ImageAttrs"] @@ -71,7 +76,7 @@ def new( metadata: JsonValue | None = None, global_scale: Sequence[float] | None = None, global_translation: Sequence[float] | None = None, - ) -> "Image": + ) -> Image: """ Create a new `Image` from a sequence of multiscale arrays and spatial metadata. diff --git a/src/ome_zarr_models/v05/image_label.py b/src/ome_zarr_models/v05/image_label.py index 01138ac2..9297d70f 100644 --- a/src/ome_zarr_models/v05/image_label.py +++ b/src/ome_zarr_models/v05/image_label.py @@ -1,8 +1,9 @@ -from typing import Self +from __future__ import annotations + +from typing import TYPE_CHECKING, Self # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr from pydantic import Field from ome_zarr_models.v05.base import BaseGroupv05, BaseOMEAttrs @@ -10,6 +11,9 @@ from ome_zarr_models.v05.image_label_types import Label from ome_zarr_models.v05.multiscales import Multiscale +if TYPE_CHECKING: + import zarr + __all__ = ["ImageLabel", "ImageLabelAttrs"] diff --git a/src/ome_zarr_models/v05/labels.py b/src/ome_zarr_models/v05/labels.py index 5b8e453e..1857e7b2 100644 --- a/src/ome_zarr_models/v05/labels.py +++ b/src/ome_zarr_models/v05/labels.py @@ -1,11 +1,11 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Self import numpy as np # Import needed for pydantic type resolution import pydantic_zarr # noqa: F401 -import zarr -import zarr.errors from pydantic import Field, ValidationError, model_validator from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, GroupSpec @@ -13,6 +13,8 @@ from ome_zarr_models.v05.base import BaseGroupv05, BaseOMEAttrs if TYPE_CHECKING: + import zarr + from ome_zarr_models.v05.image_label import ImageLabel @@ -34,7 +36,7 @@ ] -def _check_valid_dtypes(labels: "Labels") -> "Labels": +def _check_valid_dtypes(labels: Labels) -> Labels: """ Check that all multiscales levels of a labels image are valid Label data types. """ @@ -102,6 +104,12 @@ def from_zarr(cls, group: zarr.Group) -> Self: # type: ignore[override] group : zarr.Group A Zarr group that has valid OME-Zarr label metadata. """ + try: + import zarr + import zarr.errors + except ImportError as e: + raise ImportError("zarr is required to use this function") from e + from ome_zarr_models.v05.image_label import ImageLabel attrs_dict = group.attrs.asdict() @@ -144,7 +152,7 @@ def label_paths(self) -> list[str]: """ return self.attributes.ome.labels - def get_image_labels_group(self, path: str) -> "ImageLabel": + def get_image_labels_group(self, path: str) -> ImageLabel: """ Get a image labels group at a given path. """ diff --git a/src/ome_zarr_models/v05/well.py b/src/ome_zarr_models/v05/well.py index af5d330c..ac1e8a3c 100644 --- a/src/ome_zarr_models/v05/well.py +++ b/src/ome_zarr_models/v05/well.py @@ -1,14 +1,19 @@ +from __future__ import annotations + # Import needed for pydantic type resolution -from typing import Self +from typing import TYPE_CHECKING, Self import pydantic_zarr # noqa: F401 -import zarr from ome_zarr_models._utils import _from_zarr_v3 from ome_zarr_models.v05.base import BaseGroupv05, BaseOMEAttrs from ome_zarr_models.v05.image import Image from ome_zarr_models.v05.well_types import WellMeta +if TYPE_CHECKING: + import zarr + + __all__ = ["Well", "WellAttrs"] diff --git a/tests/_v06/test_general.py b/tests/_v06/test_general.py index ec533da1..cf267a8b 100644 --- a/tests/_v06/test_general.py +++ b/tests/_v06/test_general.py @@ -2,15 +2,20 @@ General tests. """ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models._v06.image import Image from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_no_ome_version_fails(store: Store) -> None: zarr_group = json_to_zarr_group( diff --git a/tests/_v06/test_hcs.py b/tests/_v06/test_hcs.py index 361a59e0..1c891ce7 100644 --- a/tests/_v06/test_hcs.py +++ b/tests/_v06/test_hcs.py @@ -1,9 +1,14 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models._v06.hcs import HCS, HCSAttrs from ome_zarr_models._v06.plate import Acquisition, Column, Plate, Row, WellInPlate from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_hcs(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="hcs_example.json", store=store) diff --git a/tests/_v06/test_image.py b/tests/_v06/test_image.py index 02e3954b..8c830eb3 100644 --- a/tests/_v06/test_image.py +++ b/tests/_v06/test_image.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models._v06.axes import Axis from ome_zarr_models._v06.coordinate_transformations import VectorScale @@ -11,6 +13,9 @@ from ome_zarr_models._v06.multiscales import Dataset, Multiscale from tests._v06.conftest import json_to_dict, json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="image_example.json", store=store) diff --git a/tests/_v06/test_image_label.py b/tests/_v06/test_image_label.py index b504ca98..d008bf55 100644 --- a/tests/_v06/test_image_label.py +++ b/tests/_v06/test_image_label.py @@ -1,4 +1,6 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models._v06.axes import Axis from ome_zarr_models._v06.coordinate_transformations import VectorScale @@ -7,6 +9,9 @@ from ome_zarr_models._v06.multiscales import Dataset, Multiscale from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image_label(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="image_label_example.json", store=store) diff --git a/tests/_v06/test_labels.py b/tests/_v06/test_labels.py index f584f95b..fb8e44be 100644 --- a/tests/_v06/test_labels.py +++ b/tests/_v06/test_labels.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import numpy as np import pytest -from zarr.abc.store import Store from ome_zarr_models._v06.labels import Labels, LabelsAttrs from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_labels(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="labels_example.json", store=store) diff --git a/tests/_v06/test_multiscales.py b/tests/_v06/test_multiscales.py index 5383c81c..08ba97ad 100644 --- a/tests/_v06/test_multiscales.py +++ b/tests/_v06/test_multiscales.py @@ -5,7 +5,6 @@ import numpy as np import pytest -import zarr from pydantic import ValidationError from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, ArraySpec, GroupSpec @@ -403,6 +402,7 @@ def test_multiscale_group_missing_arrays() -> None: """ Test that creating a multiscale group fails when an expected Zarr array is missing """ + zarr = pytest.importorskip("zarr") arrays = ( zarr.zeros((10, 10)), zarr.zeros((5, 5)), @@ -437,6 +437,7 @@ def test_multiscale_group_ectopic_group() -> None: Test that creating a multiscale group fails when an expected Zarr array is actually a group """ + zarr = pytest.importorskip("zarr") arrays = ( zarr.zeros((10, 10)), zarr.zeros((5, 5)), @@ -453,7 +454,7 @@ def test_multiscale_group_ectopic_group() -> None: ) # remove an array, then re-create the model group_model_broken = group_model.model_copy( - update={"members": {array_names[0]: GroupSpec()}} + update={"members": {array_names[0]: GroupSpec(attributes={})}} ) with pytest.raises( ValidationError, diff --git a/tests/_v06/test_plate.py b/tests/_v06/test_plate.py index c6e12a23..ecdf6cd6 100644 --- a/tests/_v06/test_plate.py +++ b/tests/_v06/test_plate.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models._v06.hcs import HCS from ome_zarr_models._v06.plate import Acquisition, Column, Plate, Row, WellInPlate from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_example_plate_json(store: Store) -> None: hcs: HCS = HCS.from_zarr( diff --git a/tests/_v06/test_well.py b/tests/_v06/test_well.py index ac2e5c30..6a8b4fe2 100644 --- a/tests/_v06/test_well.py +++ b/tests/_v06/test_well.py @@ -1,9 +1,14 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models._v06.well import Well, WellAttrs from ome_zarr_models._v06.well_types import WellImage, WellMeta from tests._v06.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_well(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="well_example.json", store=store) diff --git a/tests/conftest.py b/tests/conftest.py index 6f5cafe1..a2f4b042 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,17 +2,15 @@ import json from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Never, TypeVar +from typing import TYPE_CHECKING, Literal, TypeVar import pytest -import zarr -import zarr.storage -from zarr.abc.store import Store -from zarr.storage import LocalStore, MemoryStore from ome_zarr_models.base import BaseAttrs if TYPE_CHECKING: + from typing import Any, Never + from zarr.abc.store import Store @@ -73,27 +71,36 @@ def json_to_zarr_group( return group -class UnlistableStore(MemoryStore): - """ - A memory store that doesn't support listing. +try: + import zarr + import zarr.storage + from zarr.storage import LocalStore, MemoryStore +except ImportError: + pass +else: - Mimics other remote stores (e.g., HTTP) that don't support listing. - """ + class UnlistableStore(MemoryStore): + """ + A memory store that doesn't support listing. + + Mimics other remote stores (e.g., HTTP) that don't support listing. + """ - supports_listing: bool = False + supports_listing: bool = False - def list(self) -> Never: - raise NotImplementedError + def list(self) -> Never: + raise NotImplementedError - def list_dir(self, prefix: str) -> Never: - raise NotImplementedError + def list_dir(self, prefix: str) -> Never: + raise NotImplementedError - def list_prefix(self, prefix: str) -> Never: - raise NotImplementedError + def list_prefix(self, prefix: str) -> Never: + raise NotImplementedError @pytest.fixture(params=["MemoryStore", "LocalStore", "UnlistableStore"]) def store(request: pytest.FixtureRequest, tmp_path: Path) -> Store: + pytest.importorskip("zarr", reason="requires zarr") match request.param: case "MemoryStore": return MemoryStore() diff --git a/tests/test_cli.py b/tests/test_cli.py index e9ea5934..54b5df90 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,8 +3,6 @@ from typing import TYPE_CHECKING, cast import pytest -import zarr -from zarr.storage import LocalStore from ome_zarr_models._cli import main @@ -15,6 +13,8 @@ from pathlib import Path from typing import Any + import zarr + def populate_fake_data( zarr_group: zarr.Group, @@ -83,9 +83,11 @@ def test_cli_validate( capsys: pytest.CaptureFixture[str], ) -> None: """Test the CLI commands.""" - + zarr_storage = pytest.importorskip("zarr.storage") zarr_group = json_to_zarr_group( - version=version, json_fname=json_fname, store=LocalStore(root=tmp_path) + version=version, + json_fname=json_fname, + store=zarr_storage.LocalStore(root=tmp_path), ) populate_fake_data(zarr_group) monkeypatch.setattr("sys.argv", ["ome-zarr-models", cmd, str(tmp_path)]) @@ -102,6 +104,7 @@ def test_cli_invalid( capsys: pytest.CaptureFixture[str], ) -> None: """Test the CLI with no command.""" + zarr = pytest.importorskip("zarr") zarr.create_group(tmp_path) monkeypatch.setattr("sys.argv", ["ome-zarr-models", cmd, str(tmp_path)]) with pytest.raises(SystemExit) as excinfo: diff --git a/tests/test_root.py b/tests/test_root.py index 4549b137..f356034f 100644 --- a/tests/test_root.py +++ b/tests/test_root.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import re -from pathlib import Path +from typing import TYPE_CHECKING import pytest -import zarr -from zarr.abc.store import Store import ome_zarr_models.v04 import ome_zarr_models.v05 @@ -11,8 +11,14 @@ from tests.conftest import get_examples_path from tests.v05.test_image import make_valid_image_group +if TYPE_CHECKING: + from pathlib import Path + + from zarr.abc.store import Store + def test_load_ome_zarr_group() -> None: + zarr = pytest.importorskip("zarr") hcs_group = zarr.open_group( get_examples_path(version="0.4") / "hcs_example.ome.zarr", mode="r", @@ -40,6 +46,7 @@ def test_load_ome_zarr_group_v05_image_label(store: Store) -> None: def test_load_ome_zarr_group_bad(tmp_path: Path) -> None: + zarr = pytest.importorskip("zarr") hcs_group = zarr.create_group(tmp_path / "test") with pytest.raises( RuntimeError, @@ -52,6 +59,7 @@ def test_load_ome_zarr_group_bad(tmp_path: Path) -> None: @pytest.mark.vcr def test_load_remote_data() -> None: + pytest.importorskip("zarr") grp = open_ome_zarr( "https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.5/idr0066/ExpA_VIP_ASLM_on.zarr", version="0.5", diff --git a/tests/v04/test_hcs.py b/tests/v04/test_hcs.py index 9331721e..78f51913 100644 --- a/tests/v04/test_hcs.py +++ b/tests/v04/test_hcs.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from typing import TYPE_CHECKING -import zarr +import pytest from ome_zarr_models.common.omero import Channel, Omero, Window from ome_zarr_models.v04.axes import Axis @@ -17,6 +19,7 @@ def test_example_hcs() -> None: + zarr = pytest.importorskip("zarr") group = zarr.open_group( get_examples_path(version="0.4") / "hcs_example.ome.zarr", mode="r", @@ -146,6 +149,7 @@ def test_non_existent_wells_from_zarr() -> None: """ Same as above, but using from_zarr(...) """ + zarr = pytest.importorskip("zarr") plate: dict[str, JsonValue] = { "columns": [{"name": "1"}], "rows": [{"name": "A"}], diff --git a/tests/v04/test_image.py b/tests/v04/test_image.py index 812eb6e4..544414c0 100644 --- a/tests/v04/test_image.py +++ b/tests/v04/test_image.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import numpy as np import pytest from pydantic_zarr.v2 import ArraySpec -from zarr.abc.store import Store from ome_zarr_models.common.coordinate_transformations import VectorTranslation from ome_zarr_models.v04.axes import Axis @@ -12,6 +14,9 @@ from ome_zarr_models.v04.multiscales import Dataset, Multiscale from tests.v04.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="multiscales_example.json", store=store) @@ -77,8 +82,8 @@ def test_image(store: Store) -> None: def test_new_image() -> None: new_image = Image.new( array_specs=[ - ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8), - ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8), + ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8, attributes={}), + ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8, attributes={}), ], paths=["scale0", "scale1"], axes=[ @@ -164,8 +169,8 @@ def test_new_image() -> None: def example_image() -> Image: return Image.new( array_specs=[ - ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8), - ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8), + ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8, attributes={}), + ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8, attributes={}), ], paths=["scale0", "scale1"], axes=[ @@ -212,8 +217,8 @@ def test_new_image_wrong_transforms() -> None: ): Image.new( array_specs=[ - ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8), - ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8), + ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8, attributes={}), + ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8, attributes={}), ], paths=["scale0", "scale1"], axes=[ @@ -238,8 +243,8 @@ def test_global_transform(example_image: Image) -> None: def test_no_global_transform() -> None: new_image = Image.new( array_specs=[ - ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8), - ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8), + ArraySpec(shape=(5, 5), chunks=(2, 2), dtype=np.uint8, attributes={}), + ArraySpec(shape=(3, 3), chunks=(2, 2), dtype=np.uint8, attributes={}), ], paths=["scale0", "scale1"], axes=[ diff --git a/tests/v04/test_image_label.py b/tests/v04/test_image_label.py index 9e2fc164..d04648c5 100644 --- a/tests/v04/test_image_label.py +++ b/tests/v04/test_image_label.py @@ -1,6 +1,9 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models.v04.axes import Axis from ome_zarr_models.v04.coordinate_transformations import VectorScale @@ -14,6 +17,9 @@ from ome_zarr_models.v04.multiscales import Dataset, Multiscale from tests.v04.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image_label_example_json(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="image_label_example.json", store=store) diff --git a/tests/v04/test_labels.py b/tests/v04/test_labels.py index e28ec37c..c4d41191 100644 --- a/tests/v04/test_labels.py +++ b/tests/v04/test_labels.py @@ -1,9 +1,14 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models.v04.image import Image from ome_zarr_models.v04.labels import LabelsAttrs from tests.v04.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image_with_labels(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="multiscales_example.json", store=store) diff --git a/tests/v04/test_multiscales.py b/tests/v04/test_multiscales.py index b10eeae4..17dd9d5f 100644 --- a/tests/v04/test_multiscales.py +++ b/tests/v04/test_multiscales.py @@ -384,6 +384,7 @@ def test_multiscale_group_datasets_exist( shape=(1, 1, 1, 1), dtype="uint8", chunks=(1, 1, 1, 1), + attributes={}, ) for d in default_multiscale.datasets } @@ -394,6 +395,7 @@ def test_multiscale_group_datasets_exist( shape=(1, 1, 1, 1), dtype="uint8", chunks=(1, 1, 1, 1), + attributes={}, ) for d in default_multiscale.datasets } @@ -422,7 +424,7 @@ def test_multiscale_group_datasets_ndim() -> None: with pytest.raises(ValueError, match=re.escape(match)): Image.new( array_specs=[ - ArraySpec(shape=(10,), chunks=(10,), dtype="uint8") + ArraySpec(shape=(10,), chunks=(10,), dtype="uint8", attributes={}) for _ in range(bad_ndim) ], paths=[str(i) for i in range(true_ndim)], @@ -475,7 +477,7 @@ def test_multiscale_group_ectopic_group() -> None: ) # remove an array, then re-create the model group_model_broken = group_model.model_copy( - update={"members": {array_names[0]: GroupSpec()}} + update={"members": {array_names[0]: GroupSpec(attributes={})}} ) with pytest.raises( ValidationError, @@ -488,7 +490,7 @@ def test_from_zarr_missing_metadata( store: Store, request: pytest.FixtureRequest, ) -> None: - group_model: AnyGroupSpec = GroupSpec() + group_model: AnyGroupSpec = GroupSpec(attributes={}) group = group_model.to_zarr(store, path="test") # store_path = store.path if hasattr(store, "path") else "" match = "multiscales\n Field required" diff --git a/tests/v04/test_well.py b/tests/v04/test_well.py index 2679e564..5788c371 100644 --- a/tests/v04/test_well.py +++ b/tests/v04/test_well.py @@ -1,9 +1,14 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models.v04.well import Well, WellAttrs from ome_zarr_models.v04.well_types import WellImage, WellMeta from tests.v04.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_well(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="well_example_1.json", store=store) diff --git a/tests/v05/test_general.py b/tests/v05/test_general.py index e60cf2d4..23c063a0 100644 --- a/tests/v05/test_general.py +++ b/tests/v05/test_general.py @@ -2,15 +2,20 @@ General tests. """ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models.v05.image import Image from tests.v05.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_no_ome_version_fails(store: Store) -> None: zarr_group = json_to_zarr_group( diff --git a/tests/v05/test_hcs.py b/tests/v05/test_hcs.py index a38b7b01..4a6c3858 100644 --- a/tests/v05/test_hcs.py +++ b/tests/v05/test_hcs.py @@ -1,7 +1,8 @@ +from __future__ import annotations + from typing import TYPE_CHECKING -import zarr -from zarr.abc.store import Store +import pytest from ome_zarr_models.v05.hcs import HCS, HCSAttrs from ome_zarr_models.v05.plate import Acquisition, Column, Plate, Row, WellInPlate @@ -9,6 +10,7 @@ if TYPE_CHECKING: from pydantic import JsonValue + from zarr.abc.store import Store def test_hcs(store: Store) -> None: @@ -63,6 +65,7 @@ def test_non_existent_wells() -> None: does not specify explicitly that the Zarr groups have to exist. """ HCS( + members={}, attributes={ "ome": { "plate": { @@ -75,7 +78,7 @@ def test_non_existent_wells() -> None: }, "version": "0.5", } - } + }, ) @@ -83,7 +86,7 @@ def test_non_existent_wells_from_zarr() -> None: """ Same as above, but using from_zarr(...) """ - + zarr = pytest.importorskip("zarr") plate: dict[str, JsonValue] = { "columns": [{"name": "1"}], "rows": [{"name": "A"}], diff --git a/tests/v05/test_image.py b/tests/v05/test_image.py index d6eabb72..64a743c5 100644 --- a/tests/v05/test_image.py +++ b/tests/v05/test_image.py @@ -1,9 +1,10 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest -import zarr from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models.v05.axes import Axis from ome_zarr_models.v05.coordinate_transformations import VectorScale @@ -12,6 +13,10 @@ from ome_zarr_models.v05.multiscales import Dataset, Multiscale from tests.v05.conftest import json_to_dict, json_to_zarr_group +if TYPE_CHECKING: + import zarr + from zarr.abc.store import Store + def make_valid_image_group(store: Store) -> zarr.Group: zarr_group = json_to_zarr_group(json_fname="image_example.json", store=store) diff --git a/tests/v05/test_image_label.py b/tests/v05/test_image_label.py index e38bad70..ec0b96a7 100644 --- a/tests/v05/test_image_label.py +++ b/tests/v05/test_image_label.py @@ -1,4 +1,6 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models.v05.axes import Axis from ome_zarr_models.v05.coordinate_transformations import VectorScale @@ -7,6 +9,9 @@ from ome_zarr_models.v05.multiscales import Dataset, Multiscale from tests.v05.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_image_label(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="image_label_example.json", store=store) diff --git a/tests/v05/test_labels.py b/tests/v05/test_labels.py index 0f0b605f..6e1ca0d3 100644 --- a/tests/v05/test_labels.py +++ b/tests/v05/test_labels.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import numpy as np import pytest -from zarr.abc.store import Store from ome_zarr_models.v05.labels import Labels, LabelsAttrs from tests.v05.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_labels(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="labels_example.json", store=store) diff --git a/tests/v05/test_multiscales.py b/tests/v05/test_multiscales.py index 24593927..0b8004cd 100644 --- a/tests/v05/test_multiscales.py +++ b/tests/v05/test_multiscales.py @@ -5,7 +5,6 @@ import numpy as np import pytest -import zarr from pydantic import ValidationError from pydantic_zarr.v3 import AnyArraySpec, AnyGroupSpec, ArraySpec, GroupSpec @@ -403,6 +402,7 @@ def test_multiscale_group_missing_arrays() -> None: """ Test that creating a multiscale group fails when an expected Zarr array is missing """ + zarr = pytest.importorskip("zarr") arrays = ( zarr.zeros((10, 10)), zarr.zeros((5, 5)), @@ -437,6 +437,7 @@ def test_multiscale_group_ectopic_group() -> None: Test that creating a multiscale group fails when an expected Zarr array is actually a group """ + zarr = pytest.importorskip("zarr") arrays = ( zarr.zeros((10, 10)), zarr.zeros((5, 5)), @@ -453,7 +454,7 @@ def test_multiscale_group_ectopic_group() -> None: ) # remove an array, then re-create the model group_model_broken = group_model.model_copy( - update={"members": {array_names[0]: GroupSpec()}} + update={"members": {array_names[0]: GroupSpec(attributes={})}} ) with pytest.raises( ValidationError, diff --git a/tests/v05/test_plate.py b/tests/v05/test_plate.py index 81a55f61..09db4edf 100644 --- a/tests/v05/test_plate.py +++ b/tests/v05/test_plate.py @@ -1,13 +1,18 @@ +from __future__ import annotations + import re +from typing import TYPE_CHECKING import pytest from pydantic import ValidationError -from zarr.abc.store import Store from ome_zarr_models.v05.hcs import HCS from ome_zarr_models.v05.plate import Acquisition, Column, Plate, Row, WellInPlate from tests.v05.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_example_plate_json(store: Store) -> None: hcs: HCS = HCS.from_zarr( diff --git a/tests/v05/test_well.py b/tests/v05/test_well.py index e8c3abe9..7743fc6b 100644 --- a/tests/v05/test_well.py +++ b/tests/v05/test_well.py @@ -1,9 +1,14 @@ -from zarr.abc.store import Store +from __future__ import annotations + +from typing import TYPE_CHECKING from ome_zarr_models.v05.well import Well, WellAttrs from ome_zarr_models.v05.well_types import WellImage, WellMeta from tests.v05.conftest import json_to_zarr_group +if TYPE_CHECKING: + from zarr.abc.store import Store + def test_well(store: Store) -> None: zarr_group = json_to_zarr_group(json_fname="well_example.json", store=store)