Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
strategy:
matrix:
python-version: ['3.11', '3.12', '3.13']
zarr-version: ['3.0.10', '3.1.0']
zarr-version: ['3.0.10', '3.1.0', 'none']
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}

Expand All @@ -36,10 +36,16 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install hatch
- name: Run Tests
- name: Run Tests (with zarr)
if: matrix.zarr-version != 'none'
run: |
hatch run test.py${{ matrix.python-version }}-${{ matrix.zarr-version }}:list-env
hatch run test.py${{ matrix.python-version }}-${{ matrix.zarr-version }}:test-cov
- name: Run Tests (without zarr)
if: matrix.zarr-version == 'none'
run: |
hatch run test-base.py${{ matrix.python-version }}:list-env
hatch run test-base.py${{ matrix.python-version }}:test-cov
Comment on lines +44 to +48
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@d-v-b, let me know if you have a better idea for doing the hatch matrix here... or if you're happy to switch this repo over to using uv and do it similarly to ome-zarr-models/ome-zarr-models-py#280

- name: Upload coverage
uses: codecov/codecov-action@v5
with:
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

## Installation

`pip install -U pydantic-zarr`
```sh
pip install -U pydantic-zarr
# or, with zarr i/o support
pip install -U pydantic-zarr[zarr]
```

## Getting help

Expand Down
18 changes: 15 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
]
dependencies = ["zarr>=3", "pydantic>2.0.0"]
dependencies = ["pydantic>2.0.0", "numpy>=1.24.0"]
[project.urls]
Documentation = "https://zarr.dev/pydantic-zarr/"
Issues = "https://github.com/zarr-developers/pydantic-zarr/issues"
Source = "https://github.com/zarr-developers/pydantic-zarr"

[project.optional-dependencies]
zarr = ["zarr>=3.0.0"]
# pytest pin is due to https://github.com/pytest-dev/pytest-cov/issues/693
test = ["coverage", "pytest<8.4", "pytest-cov", "pytest-examples"]

test-base = ["coverage", "pytest<8.4", "pytest-cov", "pytest-examples"]
test = ["pydantic-zarr[test-base,zarr]"]
docs = [
"mkdocs-material",
"mkdocstrings[python]",
Expand Down Expand Up @@ -57,6 +58,17 @@ list-env = "pip list"
python = ["3.11", "3.12", "3.13"]
zarr = ["3.0.10", "3.1.0"]

[tool.hatch.envs.test-base]
features = ["test-base"]

[tool.hatch.envs.test-base.scripts]
test = "pytest tests/test_pydantic_zarr/"
test-cov = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report html --cov=src tests/test_pydantic_zarr"
list-env = "pip list"

[[tool.hatch.envs.test-base.matrix]]
python = ["3.11", "3.12", "3.13"]

[tool.hatch.envs.docs]
features = ['docs']

Expand Down
7 changes: 4 additions & 3 deletions src/pydantic_zarr/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
import numpy as np
import numpy.typing as npt
from pydantic import BaseModel, ConfigDict
from zarr.core.sync import sync
from zarr.core.sync_group import get_node
from zarr.storage._common import make_store_path

if TYPE_CHECKING:
import zarr
Expand Down Expand Up @@ -130,6 +127,10 @@ def maybe_node(
Return the array or group found at the store / path, if an array or group exists there.
Otherwise return None.
"""
from zarr.core.sync import sync
from zarr.core.sync_group import get_node
from zarr.storage._common import make_store_path

# convert the storelike store argument to a Zarr store
spath = sync(make_store_path(store, path=path))
try:
Expand Down
51 changes: 37 additions & 14 deletions src/pydantic_zarr/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
import math
import sys
from collections.abc import Mapping
from importlib.metadata import version
from typing import (
Expand All @@ -21,16 +22,9 @@

import numpy as np
import numpy.typing as npt
import zarr
from numcodecs.abc import Codec
from packaging.version import Version
from pydantic import AfterValidator, BaseModel, field_validator, model_validator
from pydantic.functional_validators import BeforeValidator
from zarr.core.array import Array, AsyncArray
from zarr.core.metadata import ArrayV2Metadata
from zarr.core.sync import sync
from zarr.errors import ContainsArrayError, ContainsGroupError
from zarr.storage._common import make_store_path

from pydantic_zarr.core import (
IncEx,
Expand All @@ -42,6 +36,8 @@
)

if TYPE_CHECKING:
import zarr
from numcodecs.abc import Codec
from zarr.abc.store import Store
from zarr.core.array_spec import ArrayConfigParams

Expand Down Expand Up @@ -90,7 +86,8 @@ def dictify_codec(value: dict[str, Any] | Codec) -> dict[str, Any]:
object is returned. This should be a dict with string keys. All other values pass
through unaltered.
"""
if isinstance(value, Codec):

if (numcodecs := sys.modules.get("numcodecs")) and isinstance(value, numcodecs.abc.Codec):
return value.get_config()
return value

Expand Down Expand Up @@ -168,7 +165,7 @@ class ArraySpec(NodeSpec, Generic[TAttr]):
The default is "/".
"""

attributes: TAttr = cast(TAttr, {})
attributes: TAttr = cast("TAttr", {})
shape: tuple[int, ...]
chunks: tuple[int, ...]
dtype: DtypeStr | list[tuple[Any, ...]]
Expand Down Expand Up @@ -334,6 +331,11 @@ def from_zarr(cls, array: zarr.Array) -> Self:
ArraySpec(zarr_format=2, attributes={}, shape=(10, 10), chunks=(10, 10), dtype='<f8', fill_value=0.0, order='C', filters=None, dimension_separator='.', compressor={'id': 'blosc', 'cname': 'lz4', 'clevel': 5, 'shuffle': 1, 'blocksize': 0})

"""
try:
from zarr.core.metadata import ArrayV2Metadata
except ImportError as e:
raise ImportError("zarr must be installed to use from_zarr") from e

if not isinstance(array.metadata, ArrayV2Metadata):
msg = "Array is not a Zarr format 2 array"
raise TypeError(msg)
Expand Down Expand Up @@ -378,6 +380,16 @@ def to_zarr(
zarr.Array
A Zarr array that is structurally identical to `self`.
"""
try:
import zarr
from zarr.core.array import Array, AsyncArray
from zarr.core.metadata import ArrayV2Metadata
from zarr.core.sync import sync
from zarr.errors import ContainsArrayError, ContainsGroupError
from zarr.storage._common import make_store_path
except ImportError as e:
raise ImportError("zarr must be installed to use to_zarr") from e

store_path = sync(make_store_path(store, path=path))

extant_node = maybe_node(store, path, zarr_format=2)
Expand Down Expand Up @@ -449,10 +461,10 @@ def like(
"""

other_parsed: ArraySpec
if isinstance(other, zarr.Array):
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Array):
other_parsed = ArraySpec.from_zarr(other)
else:
other_parsed = other
other_parsed = other # type: ignore[assignment]

return model_like(self, other_parsed, include=include, exclude=exclude)

Expand All @@ -476,7 +488,7 @@ class can be found in the
are either `ArraySpec` or `GroupSpec`.
"""

attributes: TAttr = cast(TAttr, {})
attributes: TAttr = cast("TAttr", {})
members: Annotated[Mapping[str, TItem] | None, AfterValidator(ensure_key_no_path)] = {}

@classmethod
Expand Down Expand Up @@ -505,6 +517,10 @@ def from_zarr(cls, group: zarr.Group, *, depth: int = -1) -> Self:
-------
An instance of GroupSpec that represents the structure of the Zarr hierarchy.
"""
try:
import zarr
except ImportError as e:
raise ImportError("zarr must be installed to use from_zarr") from e

result: GroupSpec[TAttr, TItem]
attributes = group.attrs.asdict()
Expand Down Expand Up @@ -563,6 +579,12 @@ def to_zarr(
A zarr group that is structurally identical to `self`.

"""
try:
import zarr
from zarr.errors import ContainsArrayError, ContainsGroupError
except ImportError as e:
raise ImportError("zarr must be installed to use to_zarr") from e

spec_dict = self.model_dump(exclude={"members": True})
attrs = spec_dict.pop("attributes")
extant_node = maybe_node(store, path, zarr_format=2)
Expand Down Expand Up @@ -660,10 +682,10 @@ def like(
"""

other_parsed: GroupSpec
if isinstance(other, zarr.Group):
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Group):
other_parsed = GroupSpec.from_zarr(other)
else:
other_parsed = other
other_parsed = other # type: ignore[assignment]

return model_like(self, other_parsed, include=include, exclude=exclude)

Expand Down Expand Up @@ -764,6 +786,7 @@ def from_zarr(element: zarr.Array | zarr.Group, depth: int = -1) -> AnyArraySpec
An instance of `GroupSpec` or `ArraySpec` that models the structure of the input Zarr group
or array.
"""
import zarr

if isinstance(element, zarr.Array):
return ArraySpec.from_zarr(element)
Expand Down
37 changes: 26 additions & 11 deletions src/pydantic_zarr/v3.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import sys
from collections.abc import Callable, Mapping
from importlib.metadata import version
from typing import (
Expand All @@ -20,11 +21,9 @@

import numpy as np
import numpy.typing as npt
import zarr
from packaging.version import Version
from pydantic import AfterValidator, BaseModel, BeforeValidator
from typing_extensions import TypedDict
from zarr.errors import ContainsArrayError, ContainsGroupError

from pydantic_zarr.core import (
IncEx,
Expand All @@ -40,6 +39,7 @@
from collections.abc import Sequence

import numpy.typing as npt
import zarr # noqa: TC004
from zarr.abc.store import Store
from zarr.core.array_spec import ArrayConfigParams

Expand Down Expand Up @@ -201,7 +201,7 @@ class ArraySpec(NodeSpec, Generic[TAttr]):
"""

node_type: Literal["array"] = "array"
attributes: TAttr = cast(TAttr, {})
attributes: TAttr = cast("TAttr", {})
shape: tuple[int, ...]
data_type: DTypeLike
chunk_grid: RegularChunking # todo: validate this against shape
Expand Down Expand Up @@ -280,7 +280,7 @@ def from_array(

"""
if attributes == "auto":
attributes_actual = cast(TAttr, auto_attributes(array))
attributes_actual = cast("TAttr", auto_attributes(array))
else:
attributes_actual = attributes

Expand Down Expand Up @@ -352,9 +352,10 @@ def from_zarr(cls, array: zarr.Array) -> Self:
ArraySpec(zarr_format=2, attributes={}, shape=(10, 10), chunks=(10, 10), dtype='<f8', fill_value=0.0, order='C', filters=None, dimension_separator='.', compressor={'id': 'blosc', 'cname': 'lz4', 'clevel': 5, 'shuffle': 1, 'blocksize': 0})

"""
meta_json: Mapping[str, object]
from zarr.core.metadata import ArrayV3Metadata

meta_json: Mapping[str, object]

if not isinstance(array.metadata, ArrayV3Metadata):
raise ValueError("Only zarr v3 arrays are supported") # noqa: TRY004
if Version(version("zarr")) < Version("3.1.0"):
Expand Down Expand Up @@ -407,10 +408,15 @@ def to_zarr(
A zarr array that is structurally identical to the ArraySpec.
This operation will create metadata documents in the store.
"""
from zarr.core.array import Array, AsyncArray
from zarr.core.metadata.v3 import ArrayV3Metadata
from zarr.core.sync import sync
from zarr.storage._common import make_store_path
try:
import zarr
from zarr.core.array import Array, AsyncArray
from zarr.core.metadata.v3 import ArrayV3Metadata
from zarr.core.sync import sync
from zarr.errors import ContainsArrayError, ContainsGroupError
from zarr.storage._common import make_store_path
except ImportError as e:
raise ImportError("zarr must be installed to use this method") from e

store_path = sync(make_store_path(store, path=path))
extant_node = maybe_node(store, path, zarr_format=3)
Expand Down Expand Up @@ -620,6 +626,10 @@ def from_zarr(cls, group: zarr.Group, *, depth: int = -1) -> Self:
-------
An instance of GroupSpec that represents the structure of the zarr hierarchy.
"""
try:
import zarr
except ImportError as e:
raise ImportError("zarr must be installed to use from_zarr") from e

result: GroupSpec[TAttr, TItem]
attributes = group.attrs.asdict()
Expand Down Expand Up @@ -675,6 +685,11 @@ def to_zarr(
A zarr group that is structurally identical to the GroupSpec.
This operation will create metadata documents in the store.
"""
try:
import zarr
from zarr.errors import ContainsArrayError, ContainsGroupError
except ImportError as e:
raise ImportError("zarr must be installed to use this method") from e

spec_dict = self.model_dump(exclude={"members": True})
attrs = spec_dict.pop("attributes")
Expand Down Expand Up @@ -776,10 +791,10 @@ def like(
"""

other_parsed: GroupSpec[Any, Any]
if isinstance(other, zarr.Group):
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Group):
other_parsed = GroupSpec.from_zarr(other)
else:
other_parsed = other
other_parsed = other # type: ignore[assignment]

return model_like(self, other_parsed, include=include, exclude=exclude)

Expand Down
2 changes: 2 additions & 0 deletions tests/test_docs/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ def test_docstrings(example: CodeExample, eval_example: EvalExample) -> None:

@pytest.mark.parametrize("example", find_examples("docs"), ids=str)
def test_docs_examples(example: CodeExample, eval_example: EvalExample) -> None:
pytest.importorskip("zarr")

eval_example.run_print_check(example)
8 changes: 6 additions & 2 deletions tests/test_pydantic_zarr/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import warnings
from dataclasses import dataclass
from importlib.metadata import version
from importlib.metadata import PackageNotFoundError, version

from packaging.version import Version

ZARR_PYTHON_VERSION = Version(version("zarr"))
try:
ZARR_PYTHON_VERSION = Version(version("zarr"))
except PackageNotFoundError:
ZARR_PYTHON_VERSION = Version("0.0.0")

DTYPE_EXAMPLES_V2: tuple[DTypeExample, ...]
DTYPE_EXAMPLES_V3: tuple[DTypeExample, ...]

Expand Down
Loading
Loading