Skip to content

Commit 96cc629

Browse files
authored
Merge pull request #112 from tlambert03/drop-zarr
build: make zarr dependency optional
2 parents 0b9bd1d + 84707cc commit 96cc629

File tree

10 files changed

+153
-48
lines changed

10 files changed

+153
-48
lines changed

.github/workflows/test.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
strategy:
2222
matrix:
2323
python-version: ['3.11', '3.12', '3.13']
24-
zarr-version: ['3.0.10', '3.1.0']
24+
zarr-version: ['3.0.10', '3.1.0', 'none']
2525
os: ["ubuntu-latest"]
2626
runs-on: ${{ matrix.os }}
2727

@@ -36,10 +36,16 @@ jobs:
3636
run: |
3737
python -m pip install --upgrade pip
3838
pip install hatch
39-
- name: Run Tests
39+
- name: Run Tests (with zarr)
40+
if: matrix.zarr-version != 'none'
4041
run: |
4142
hatch run test.py${{ matrix.python-version }}-${{ matrix.zarr-version }}:list-env
4243
hatch run test.py${{ matrix.python-version }}-${{ matrix.zarr-version }}:test-cov
44+
- name: Run Tests (without zarr)
45+
if: matrix.zarr-version == 'none'
46+
run: |
47+
hatch run test-base.py${{ matrix.python-version }}:list-env
48+
hatch run test-base.py${{ matrix.python-version }}:test-cov
4349
- name: Upload coverage
4450
uses: codecov/codecov-action@v5
4551
with:

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
## Installation
88

9-
`pip install -U pydantic-zarr`
9+
```sh
10+
pip install -U pydantic-zarr
11+
# or, with zarr i/o support
12+
pip install -U "pydantic-zarr[zarr]"
13+
```
1014

1115
## Getting help
1216

pyproject.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ classifiers = [
2020
"Programming Language :: Python :: 3.12",
2121
"Programming Language :: Python :: Implementation :: CPython",
2222
]
23-
dependencies = ["zarr>=3", "pydantic>2.0.0"]
23+
dependencies = ["pydantic>2.0.0", "numpy>=1.24.0"]
2424
[project.urls]
2525
Documentation = "https://zarr.dev/pydantic-zarr/"
2626
Issues = "https://github.com/zarr-developers/pydantic-zarr/issues"
2727
Source = "https://github.com/zarr-developers/pydantic-zarr"
2828

2929
[project.optional-dependencies]
30+
zarr = ["zarr>=3.0.0"]
3031
# pytest pin is due to https://github.com/pytest-dev/pytest-cov/issues/693
31-
test = ["coverage", "pytest<8.4", "pytest-cov", "pytest-examples"]
32-
32+
test-base = ["coverage", "pytest<8.4", "pytest-cov", "pytest-examples"]
33+
test = ["pydantic-zarr[test-base,zarr]"]
3334
docs = [
3435
"mkdocs-material",
3536
"mkdocstrings[python]",
@@ -57,6 +58,17 @@ list-env = "pip list"
5758
python = ["3.11", "3.12", "3.13"]
5859
zarr = ["3.0.10", "3.1.0"]
5960

61+
[tool.hatch.envs.test-base]
62+
features = ["test-base"]
63+
64+
[tool.hatch.envs.test-base.scripts]
65+
test = "pytest tests/test_pydantic_zarr/"
66+
test-cov = "pytest --cov-config=pyproject.toml --cov=pkg --cov-report html --cov=src tests/test_pydantic_zarr"
67+
list-env = "pip list"
68+
69+
[[tool.hatch.envs.test-base.matrix]]
70+
python = ["3.11", "3.12", "3.13"]
71+
6072
[tool.hatch.envs.docs]
6173
features = ['docs']
6274

src/pydantic_zarr/core.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313
import numpy as np
1414
import numpy.typing as npt
1515
from pydantic import BaseModel, ConfigDict
16-
from zarr.core.sync import sync
17-
from zarr.core.sync_group import get_node
18-
from zarr.storage._common import make_store_path
1916

2017
if TYPE_CHECKING:
2118
import zarr
@@ -130,6 +127,10 @@ def maybe_node(
130127
Return the array or group found at the store / path, if an array or group exists there.
131128
Otherwise return None.
132129
"""
130+
from zarr.core.sync import sync
131+
from zarr.core.sync_group import get_node
132+
from zarr.storage._common import make_store_path
133+
133134
# convert the storelike store argument to a Zarr store
134135
spath = sync(make_store_path(store, path=path))
135136
try:

src/pydantic_zarr/v2.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import math
5+
import sys
56
from collections.abc import Mapping
67
from importlib.metadata import version
78
from typing import (
@@ -21,16 +22,9 @@
2122

2223
import numpy as np
2324
import numpy.typing as npt
24-
import zarr
25-
from numcodecs.abc import Codec
2625
from packaging.version import Version
2726
from pydantic import AfterValidator, BaseModel, field_validator, model_validator
2827
from pydantic.functional_validators import BeforeValidator
29-
from zarr.core.array import Array, AsyncArray
30-
from zarr.core.metadata import ArrayV2Metadata
31-
from zarr.core.sync import sync
32-
from zarr.errors import ContainsArrayError, ContainsGroupError
33-
from zarr.storage._common import make_store_path
3428

3529
from pydantic_zarr.core import (
3630
IncEx,
@@ -42,6 +36,8 @@
4236
)
4337

4438
if TYPE_CHECKING:
39+
import zarr
40+
from numcodecs.abc import Codec
4541
from zarr.abc.store import Store
4642
from zarr.core.array_spec import ArrayConfigParams
4743

@@ -90,7 +86,8 @@ def dictify_codec(value: dict[str, Any] | Codec) -> dict[str, Any]:
9086
object is returned. This should be a dict with string keys. All other values pass
9187
through unaltered.
9288
"""
93-
if isinstance(value, Codec):
89+
90+
if (numcodecs := sys.modules.get("numcodecs")) and isinstance(value, numcodecs.abc.Codec):
9491
return value.get_config()
9592
return value
9693

@@ -334,6 +331,11 @@ def from_zarr(cls, array: zarr.Array) -> Self:
334331
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})
335332
336333
"""
334+
try:
335+
from zarr.core.metadata import ArrayV2Metadata
336+
except ImportError as e:
337+
raise ImportError("zarr must be installed to use from_zarr") from e
338+
337339
if not isinstance(array.metadata, ArrayV2Metadata):
338340
msg = "Array is not a Zarr format 2 array"
339341
raise TypeError(msg)
@@ -378,6 +380,16 @@ def to_zarr(
378380
zarr.Array
379381
A Zarr array that is structurally identical to `self`.
380382
"""
383+
try:
384+
import zarr
385+
from zarr.core.array import Array, AsyncArray
386+
from zarr.core.metadata import ArrayV2Metadata
387+
from zarr.core.sync import sync
388+
from zarr.errors import ContainsArrayError, ContainsGroupError
389+
from zarr.storage._common import make_store_path
390+
except ImportError as e:
391+
raise ImportError("zarr must be installed to use to_zarr") from e
392+
381393
store_path = sync(make_store_path(store, path=path))
382394

383395
extant_node = maybe_node(store, path, zarr_format=2)
@@ -449,10 +461,10 @@ def like(
449461
"""
450462

451463
other_parsed: ArraySpec
452-
if isinstance(other, zarr.Array):
464+
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Array):
453465
other_parsed = ArraySpec.from_zarr(other)
454466
else:
455-
other_parsed = other
467+
other_parsed = other # type: ignore[assignment]
456468

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

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

509525
result: GroupSpec[TAttr, TItem]
510526
attributes = group.attrs.asdict()
@@ -563,6 +579,12 @@ def to_zarr(
563579
A zarr group that is structurally identical to `self`.
564580
565581
"""
582+
try:
583+
import zarr
584+
from zarr.errors import ContainsArrayError, ContainsGroupError
585+
except ImportError as e:
586+
raise ImportError("zarr must be installed to use to_zarr") from e
587+
566588
spec_dict = self.model_dump(exclude={"members": True})
567589
attrs = spec_dict.pop("attributes")
568590
extant_node = maybe_node(store, path, zarr_format=2)
@@ -660,10 +682,10 @@ def like(
660682
"""
661683

662684
other_parsed: GroupSpec
663-
if isinstance(other, zarr.Group):
685+
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Group):
664686
other_parsed = GroupSpec.from_zarr(other)
665687
else:
666-
other_parsed = other
688+
other_parsed = other # type: ignore[assignment]
667689

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

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

768791
if isinstance(element, zarr.Array):
769792
return ArraySpec.from_zarr(element)

src/pydantic_zarr/v3.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import sys
45
from collections.abc import Callable, Mapping
56
from importlib.metadata import version
67
from typing import (
@@ -20,11 +21,9 @@
2021

2122
import numpy as np
2223
import numpy.typing as npt
23-
import zarr
2424
from packaging.version import Version
2525
from pydantic import AfterValidator, BaseModel, BeforeValidator
2626
from typing_extensions import TypedDict
27-
from zarr.errors import ContainsArrayError, ContainsGroupError
2827

2928
from pydantic_zarr.core import (
3029
IncEx,
@@ -40,6 +39,7 @@
4039
from collections.abc import Sequence
4140

4241
import numpy.typing as npt
42+
import zarr # noqa: TC004
4343
from zarr.abc.store import Store
4444
from zarr.core.array_spec import ArrayConfigParams
4545

@@ -280,7 +280,7 @@ def from_array(
280280
281281
"""
282282
if attributes == "auto":
283-
attributes_actual = cast(TAttr, auto_attributes(array))
283+
attributes_actual = cast("TAttr", auto_attributes(array))
284284
else:
285285
attributes_actual = attributes
286286

@@ -352,8 +352,12 @@ def from_zarr(cls, array: zarr.Array) -> Self:
352352
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})
353353
354354
"""
355+
try:
356+
from zarr.core.metadata import ArrayV3Metadata
357+
except ImportError as e:
358+
raise ImportError("zarr must be installed to use from_zarr") from e
359+
355360
meta_json: Mapping[str, object]
356-
from zarr.core.metadata import ArrayV3Metadata
357361

358362
if not isinstance(array.metadata, ArrayV3Metadata):
359363
raise ValueError("Only zarr v3 arrays are supported") # noqa: TRY004
@@ -407,10 +411,15 @@ def to_zarr(
407411
A zarr array that is structurally identical to the ArraySpec.
408412
This operation will create metadata documents in the store.
409413
"""
410-
from zarr.core.array import Array, AsyncArray
411-
from zarr.core.metadata.v3 import ArrayV3Metadata
412-
from zarr.core.sync import sync
413-
from zarr.storage._common import make_store_path
414+
try:
415+
import zarr
416+
from zarr.core.array import Array, AsyncArray
417+
from zarr.core.metadata.v3 import ArrayV3Metadata
418+
from zarr.core.sync import sync
419+
from zarr.errors import ContainsArrayError, ContainsGroupError
420+
from zarr.storage._common import make_store_path
421+
except ImportError as e:
422+
raise ImportError("zarr must be installed to use to_zarr") from e
414423

415424
store_path = sync(make_store_path(store, path=path))
416425
extant_node = maybe_node(store, path, zarr_format=3)
@@ -620,6 +629,10 @@ def from_zarr(cls, group: zarr.Group, *, depth: int = -1) -> Self:
620629
-------
621630
An instance of GroupSpec that represents the structure of the zarr hierarchy.
622631
"""
632+
try:
633+
import zarr
634+
except ImportError as e:
635+
raise ImportError("zarr must be installed to use from_zarr") from e
623636

624637
result: GroupSpec[TAttr, TItem]
625638
attributes = group.attrs.asdict()
@@ -675,6 +688,11 @@ def to_zarr(
675688
A zarr group that is structurally identical to the GroupSpec.
676689
This operation will create metadata documents in the store.
677690
"""
691+
try:
692+
import zarr
693+
from zarr.errors import ContainsArrayError, ContainsGroupError
694+
except ImportError as e:
695+
raise ImportError("zarr must be installed to use to_zarr") from e
678696

679697
spec_dict = self.model_dump(exclude={"members": True})
680698
attrs = spec_dict.pop("attributes")
@@ -776,10 +794,10 @@ def like(
776794
"""
777795

778796
other_parsed: GroupSpec[Any, Any]
779-
if isinstance(other, zarr.Group):
797+
if (zarr := sys.modules.get("zarr")) and isinstance(other, zarr.Group):
780798
other_parsed = GroupSpec.from_zarr(other)
781799
else:
782-
other_parsed = other
800+
other_parsed = other # type: ignore[assignment]
783801

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

tests/test_docs/test_docs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ def test_docstrings(example: CodeExample, eval_example: EvalExample) -> None:
1515

1616
@pytest.mark.parametrize("example", find_examples("docs"), ids=str)
1717
def test_docs_examples(example: CodeExample, eval_example: EvalExample) -> None:
18+
pytest.importorskip("zarr")
19+
1820
eval_example.run_print_check(example)

tests/test_pydantic_zarr/conftest.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import warnings
44
from dataclasses import dataclass
5-
from importlib.metadata import version
5+
from importlib.metadata import PackageNotFoundError, version
66

77
from packaging.version import Version
88

9-
ZARR_PYTHON_VERSION = Version(version("zarr"))
9+
try:
10+
ZARR_PYTHON_VERSION = Version(version("zarr"))
11+
except PackageNotFoundError:
12+
ZARR_PYTHON_VERSION = Version("0.0.0")
13+
1014
DTYPE_EXAMPLES_V2: tuple[DTypeExample, ...]
1115
DTYPE_EXAMPLES_V3: tuple[DTypeExample, ...]
1216

0 commit comments

Comments
 (0)