Skip to content

Commit f42a233

Browse files
d-v-bmkittidstansby
authored
Handle zarr 3.1.0 (#766)
* (chore) add type hints to codec abc * (chore) build project with pixi * use fresh abcs * remove blosc * Update pyproject.toml * Update pyproject.toml Co-authored-by: Mark Kittisopikul <[email protected]> * dtype adaptor for zarr 3.1 * revert change to pyproject.toml * versionify version * versionify another version * lint * lint * dodge test coverage * Update pyproject.toml * dtype adaptor for zarr 3.1 * revert change to pyproject.toml * versionify version * versionify another version * lint * lint * dodge test coverage * use pixi + hatch for parametrized zarr python testing * Apply suggestions from code review Co-authored-by: David Stansby <[email protected]> * privatize functions * add lockfile * pin hatch * list dependencies in ci * use less ambiguous version specifier * pass linting by masking a type error that needs to be fixed later * add pixi.lock to gitignore * remove pixi.lock * set cache=false in pixi workflow --------- Co-authored-by: Mark Kittisopikul <[email protected]> Co-authored-by: David Stansby <[email protected]>
1 parent 506c89b commit f42a233

File tree

5 files changed

+104
-12
lines changed

5 files changed

+104
-12
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# GitHub syntax highlighting
2+
pixi.lock linguist-language=YAML linguist-generated=true

.github/workflows/ci.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,33 @@ jobs:
7070
fail_ci_if_error: true
7171
token: ${{ secrets.CODECOV_TOKEN }}
7272
verbose: true
73+
74+
test-zarr:
75+
runs-on: ubuntu-latest
76+
strategy:
77+
fail-fast: false
78+
79+
defaults:
80+
run:
81+
shell: bash -el {0}
82+
83+
steps:
84+
- name: Checkout source
85+
uses: actions/checkout@v4
86+
with:
87+
submodules: recursive
88+
fetch-depth: 0 # required for version resolution
89+
90+
- name: Set up Pixi
91+
uses: prefix-dev/[email protected]
92+
with:
93+
pixi-version: v0.49.0
94+
cache: false
95+
96+
- name: List deps
97+
shell: "bash -l {0}"
98+
run: pixi run -e default hatch run test:list-deps
99+
100+
- name: Run tests
101+
shell: "bash -l {0}"
102+
run: pixi run -e default hatch run test:test-zarr

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,7 @@ numcodecs/version.py
104104

105105
# Cython generated
106106
numcodecs/*.c
107+
# pixi environments
108+
.pixi/*
109+
*.egg-info
110+
pixi.lock

numcodecs/zarr3.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,19 @@
2929
import math
3030
from dataclasses import dataclass, replace
3131
from functools import cached_property
32+
from importlib.metadata import version
3233
from typing import Any, Self
3334
from warnings import warn
3435

3536
import numpy as np
37+
from packaging.version import Version
3638

3739
import numcodecs
3840

3941
try:
40-
import zarr
42+
import zarr # noqa: F401
4143

42-
if zarr.__version__ < "3.0.0": # pragma: no cover
44+
if Version(version('zarr')) < Version("3.0.0"): # pragma: no cover
4345
raise ImportError("zarr 3.0.0 or later is required to use the numcodecs zarr integration.")
4446
except ImportError as e: # pragma: no cover
4547
raise ImportError(
@@ -56,6 +58,23 @@
5658
CODEC_PREFIX = "numcodecs."
5759

5860

61+
def _from_zarr_dtype(dtype: Any) -> np.dtype:
62+
"""
63+
Get a numpy data type from an array spec, depending on the zarr version.
64+
"""
65+
if Version(version('zarr')) >= Version("3.1.0"):
66+
return dtype.to_native_dtype()
67+
return dtype # pragma: no cover
68+
69+
70+
def _to_zarr_dtype(dtype: np.dtype) -> Any:
71+
if Version(version('zarr')) >= Version("3.1.0"):
72+
from zarr.dtype import parse_data_type
73+
74+
return parse_data_type(dtype, zarr_format=3)
75+
return dtype # pragma: no cover
76+
77+
5978
def _expect_name_prefix(codec_name: str) -> str:
6079
if not codec_name.startswith(CODEC_PREFIX):
6180
raise ValueError(
@@ -224,15 +243,17 @@ class LZMA(_NumcodecsBytesBytesCodec, codec_name="lzma"):
224243
class Shuffle(_NumcodecsBytesBytesCodec, codec_name="shuffle"):
225244
def evolve_from_array_spec(self, array_spec: ArraySpec) -> Shuffle:
226245
if self.codec_config.get("elementsize") is None:
227-
return Shuffle(**{**self.codec_config, "elementsize": array_spec.dtype.itemsize})
246+
dtype = _from_zarr_dtype(array_spec.dtype)
247+
return Shuffle(**{**self.codec_config, "elementsize": dtype.itemsize})
228248
return self # pragma: no cover
229249

230250

231251
# array-to-array codecs ("filters")
232252
class Delta(_NumcodecsArrayArrayCodec, codec_name="delta"):
233253
def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec:
234254
if astype := self.codec_config.get("astype"):
235-
return replace(chunk_spec, dtype=np.dtype(astype)) # type: ignore[call-overload]
255+
dtype = _to_zarr_dtype(np.dtype(astype)) # type: ignore[call-overload]
256+
return replace(chunk_spec, dtype=dtype)
236257
return chunk_spec
237258

238259

@@ -243,12 +264,14 @@ class BitRound(_NumcodecsArrayArrayCodec, codec_name="bitround"):
243264
class FixedScaleOffset(_NumcodecsArrayArrayCodec, codec_name="fixedscaleoffset"):
244265
def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec:
245266
if astype := self.codec_config.get("astype"):
246-
return replace(chunk_spec, dtype=np.dtype(astype)) # type: ignore[call-overload]
267+
dtype = _to_zarr_dtype(np.dtype(astype)) # type: ignore[call-overload]
268+
return replace(chunk_spec, dtype=dtype)
247269
return chunk_spec
248270

249271
def evolve_from_array_spec(self, array_spec: ArraySpec) -> FixedScaleOffset:
250272
if self.codec_config.get("dtype") is None:
251-
return FixedScaleOffset(**{**self.codec_config, "dtype": str(array_spec.dtype)})
273+
dtype = _from_zarr_dtype(array_spec.dtype)
274+
return FixedScaleOffset(**{**self.codec_config, "dtype": str(dtype)})
252275
return self
253276

254277

@@ -258,7 +281,8 @@ def __init__(self, **codec_config: JSON) -> None:
258281

259282
def evolve_from_array_spec(self, array_spec: ArraySpec) -> Quantize:
260283
if self.codec_config.get("dtype") is None:
261-
return Quantize(**{**self.codec_config, "dtype": str(array_spec.dtype)})
284+
dtype = _from_zarr_dtype(array_spec.dtype)
285+
return Quantize(**{**self.codec_config, "dtype": str(dtype)})
262286
return self
263287

264288

@@ -267,21 +291,27 @@ def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec:
267291
return replace(
268292
chunk_spec,
269293
shape=(1 + math.ceil(product(chunk_spec.shape) / 8),),
270-
dtype=np.dtype("uint8"),
294+
dtype=_to_zarr_dtype(np.dtype("uint8")),
271295
)
272296

273-
def validate(self, *, dtype: np.dtype[Any], **_kwargs) -> None:
274-
if dtype != np.dtype("bool"):
297+
# todo: remove this type: ignore when this class can be defined w.r.t.
298+
# a single zarr dtype API
299+
def validate(self, *, dtype: np.dtype[Any], **_kwargs) -> None: # type: ignore[override]
300+
_dtype = _from_zarr_dtype(dtype)
301+
if _dtype != np.dtype("bool"):
275302
raise ValueError(f"Packbits filter requires bool dtype. Got {dtype}.")
276303

277304

278305
class AsType(_NumcodecsArrayArrayCodec, codec_name="astype"):
279306
def resolve_metadata(self, chunk_spec: ArraySpec) -> ArraySpec:
280-
return replace(chunk_spec, dtype=np.dtype(self.codec_config["encode_dtype"])) # type: ignore[arg-type]
307+
dtype = _to_zarr_dtype(np.dtype(self.codec_config["encode_dtype"])) # type: ignore[arg-type]
308+
return replace(chunk_spec, dtype=dtype)
281309

282310
def evolve_from_array_spec(self, array_spec: ArraySpec) -> AsType:
283311
if self.codec_config.get("decode_dtype") is None:
284-
return AsType(**{**self.codec_config, "decode_dtype": str(array_spec.dtype)})
312+
# TODO: remove these coverage exemptions the correct way, i.e. with tests
313+
dtype = _from_zarr_dtype(array_spec.dtype) # pragma: no cover
314+
return AsType(**{**self.codec_config, "decode_dtype": str(dtype)}) # pragma: no cover
285315
return self
286316

287317

pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,29 @@ warn_unreachable = false
241241
warn_redundant_casts = true
242242
warn_unused_ignores = true
243243
warn_unused_configs = true
244+
245+
[tool.pixi.project]
246+
channels = ["conda-forge"]
247+
platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"]
248+
249+
[tool.pixi.dependencies]
250+
clang = ">=19.1.7,<20"
251+
c-compiler = ">=1.9.0,<2"
252+
cxx-compiler = ">=1.9.0,<2"
253+
hatch = '==1.14.1'
254+
255+
[[tool.hatch.envs.test.matrix]]
256+
python = ["3.11"]
257+
zarr = ["3.0.10", "3.1.0"]
258+
259+
[tool.hatch.envs.test]
260+
dependencies = [
261+
"zarr=={matrix:zarr}"
262+
]
263+
numpy="==2.2"
264+
features = ["test"]
265+
266+
267+
[tool.hatch.envs.test.scripts]
268+
list-deps = "pip list"
269+
test-zarr = "pytest numcodecs/tests/test_zarr3.py numcodecs/tests/test_zarr3_import.py"

0 commit comments

Comments
 (0)