Skip to content

Commit 5a975fd

Browse files
authored
Merge branch 'main' into chore/add-zizimor
2 parents be84ea6 + c9b534a commit 5a975fd

File tree

10 files changed

+305
-20
lines changed

10 files changed

+305
-20
lines changed

.github/workflows/gpu_test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ jobs:
7979
uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1
8080
with:
8181
token: ${{ secrets.CODECOV_TOKEN }}
82+
flags: gpu
8283
verbose: true # optional (default = false)

.github/workflows/hypothesis.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ jobs:
9696
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
9797
with:
9898
token: ${{ secrets.CODECOV_TOKEN }}
99+
flags: tests
99100
verbose: true # optional (default = false)
100101

101102
- name: Generate and publish the report

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
8282
with:
8383
token: ${{ secrets.CODECOV_TOKEN }}
84+
flags: tests
8485
verbose: true # optional (default = false)
8586

8687
test-upstream-and-min-deps:
@@ -127,6 +128,7 @@ jobs:
127128
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
128129
with:
129130
token: ${{ secrets.CODECOV_TOKEN }}
131+
flags: tests
130132
verbose: true # optional (default = false)
131133

132134
doctests:

changes/2720.doc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Document removal of `zarr.storage.init_group` in v3 migration guide, with replacement using `zarr.open_group`/`zarr.create_group`.

codecov.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,21 @@ coverage:
88
default:
99
target: auto
1010
threshold: 0.1
11+
flags:
12+
- tests
13+
flags:
14+
tests:
15+
paths:
16+
- src/
17+
carryforward: true
18+
gpu:
19+
paths:
20+
- src/
21+
carryforward: true
1122
codecov:
1223
notify:
13-
after_n_builds: 10 # Wait for all 10 reports before updating the status
24+
# 6 = test.yml: 3 (optional+ubuntu) + 2 (upstream + min_deps), hypothesis: 1
25+
after_n_builds: 6
1426
wait_for_ci: yes
1527
comment:
1628
layout: "diff, files"

docs/user-guide/v3_migration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ The following sections provide details on breaking changes in Zarr-Python 3.
114114
- Use [`zarr.Group.require_array`][] in place of `zarr.Group.require_dataset`
115115
3. Disallow "." syntax for getting group members. To get a member of a group named `foo`,
116116
use `group["foo"]` in place of `group.foo`.
117+
4. The `zarr.storage.init_group` low-level helper function has been removed. Use
118+
[`zarr.open_group`][] or [`zarr.create_group`][] instead:
119+
120+
```diff
121+
- from zarr.storage import init_group
122+
- init_group(store, overwrite=True, path="my/path")
123+
+ import zarr
124+
+ zarr.open_group(store, mode="w", path="my/path")
125+
```
117126

118127
### The Store class
119128

pyproject.toml

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,6 @@ hooks.vcs.version-file = "src/zarr/_version.py"
155155
dependency-groups = ["test"]
156156

157157
[tool.hatch.envs.test.env-vars]
158-
# Required to test with a pytest plugin; see https://pytest-cov.readthedocs.io/en/latest/plugins.html
159-
COV_CORE_SOURCE = "src"
160-
COV_CORE_CONFIG = ".coveragerc"
161-
COV_CORE_DATAFILE = ".coverage.eager"
162158

163159
[[tool.hatch.envs.test.matrix]]
164160
python = ["3.12", "3.13", "3.14"]
@@ -175,13 +171,23 @@ matrix.deps.dependency-groups = [
175171
]
176172

177173
[tool.hatch.envs.test.scripts]
178-
run-coverage = "pytest --cov-config=pyproject.toml --cov=src --cov-append --cov-report xml --junitxml=junit.xml -o junit_family=legacy"
179-
run-coverage-html = "pytest --cov-config=pyproject.toml --cov=src --cov-append --cov-report html"
180-
run = "run-coverage --no-cov --ignore tests/benchmarks"
174+
run-coverage = [
175+
"coverage run --source=src -m pytest --junitxml=junit.xml -o junit_family=legacy {args:}",
176+
"coverage xml",
177+
]
178+
run-coverage-html = [
179+
"coverage run --source=src -m pytest {args:}",
180+
"coverage html",
181+
]
182+
run = "pytest --ignore tests/benchmarks"
181183
run-verbose = "run-coverage --verbose"
182184
run-mypy = "mypy src"
183-
run-hypothesis = "run-coverage -nauto --run-slow-hypothesis tests/test_properties.py tests/test_store/test_stateful*"
185+
run-hypothesis = [
186+
"coverage run --source=src -m pytest -nauto --run-slow-hypothesis tests/test_properties.py tests/test_store/test_stateful* {args:}",
187+
"coverage xml",
188+
]
184189
run-benchmark = "pytest --benchmark-enable tests/benchmarks"
190+
serve-coverage-html = "python -m http.server -d htmlcov 8000"
185191
list-env = "pip list"
186192

187193
[tool.hatch.envs.gputest]
@@ -195,8 +201,11 @@ features = ["gpu"]
195201
python = ["3.12", "3.13"]
196202

197203
[tool.hatch.envs.gputest.scripts]
198-
run-coverage = "pytest -m gpu --cov-config=pyproject.toml --cov=src --cov-report xml --junitxml=junit.xml -o junit_family=legacy --ignore tests/benchmarks"
199-
run = "run-coverage --no-cov"
204+
run-coverage = [
205+
"coverage run --source=src -m pytest -m gpu --junitxml=junit.xml -o junit_family=legacy --ignore tests/benchmarks {args:}",
206+
"coverage xml",
207+
]
208+
run = "pytest -m gpu --ignore tests/benchmarks"
200209

201210
[tool.hatch.envs.upstream]
202211
template = 'test'

src/zarr/abc/codec.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,19 @@ def _check_codecjson_v2(data: object) -> TypeGuard[CodecJSON_V2[str]]:
6767

6868

6969
@runtime_checkable
70-
class SupportsSyncCodec(Protocol):
70+
class SupportsSyncCodec[CI: CodecInput, CO: CodecOutput](Protocol):
7171
"""Protocol for codecs that support synchronous encode/decode.
7272
73-
Codecs implementing this protocol provide ``_decode_sync`` and ``_encode_sync``
73+
Codecs implementing this protocol provide `_decode_sync` and `_encode_sync`
7474
methods that perform encoding/decoding without requiring an async event loop.
75+
76+
The type parameters mirror `BaseCodec`: `CI` is the decoded type and `CO` is
77+
the encoded type.
7578
"""
7679

77-
def _decode_sync(
78-
self, chunk_data: NDBuffer | Buffer, chunk_spec: ArraySpec
79-
) -> NDBuffer | Buffer: ...
80+
def _decode_sync(self, chunk_data: CO, chunk_spec: ArraySpec) -> CI: ...
8081

81-
def _encode_sync(
82-
self, chunk_data: NDBuffer | Buffer, chunk_spec: ArraySpec
83-
) -> NDBuffer | Buffer | None: ...
82+
def _encode_sync(self, chunk_data: CI, chunk_spec: ArraySpec) -> CO | None: ...
8483

8584

8685
class BaseCodec[CI: CodecInput, CO: CodecOutput](Metadata):

src/zarr/core/codec_pipeline.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass
3+
from dataclasses import dataclass, field
44
from itertools import islice, pairwise
55
from typing import TYPE_CHECKING, Any
66
from warnings import warn
@@ -14,6 +14,7 @@
1414
Codec,
1515
CodecPipeline,
1616
GetResult,
17+
SupportsSyncCodec,
1718
)
1819
from zarr.core.common import concurrent_map
1920
from zarr.core.config import config
@@ -66,6 +67,111 @@ def fill_value_or_default(chunk_spec: ArraySpec) -> Any:
6667
return fill_value
6768

6869

70+
@dataclass(slots=True, kw_only=True)
71+
class ChunkTransform:
72+
"""A synchronous codec chain bound to an ArraySpec.
73+
74+
Provides `encode` and `decode` for pure-compute codec operations
75+
(no IO, no threading, no batching).
76+
77+
All codecs must implement `SupportsSyncCodec`. Construction will
78+
raise `TypeError` if any codec does not.
79+
"""
80+
81+
codecs: tuple[Codec, ...]
82+
array_spec: ArraySpec
83+
84+
# (sync codec, input_spec) pairs in pipeline order.
85+
_aa_codecs: tuple[tuple[SupportsSyncCodec[NDBuffer, NDBuffer], ArraySpec], ...] = field(
86+
init=False, repr=False, compare=False
87+
)
88+
_ab_codec: SupportsSyncCodec[NDBuffer, Buffer] = field(init=False, repr=False, compare=False)
89+
_ab_spec: ArraySpec = field(init=False, repr=False, compare=False)
90+
_bb_codecs: tuple[SupportsSyncCodec[Buffer, Buffer], ...] = field(
91+
init=False, repr=False, compare=False
92+
)
93+
94+
def __post_init__(self) -> None:
95+
non_sync = [c for c in self.codecs if not isinstance(c, SupportsSyncCodec)]
96+
if non_sync:
97+
names = ", ".join(type(c).__name__ for c in non_sync)
98+
raise TypeError(
99+
f"All codecs must implement SupportsSyncCodec. The following do not: {names}"
100+
)
101+
102+
aa, ab, bb = codecs_from_list(list(self.codecs))
103+
104+
aa_codecs: list[tuple[SupportsSyncCodec[NDBuffer, NDBuffer], ArraySpec]] = []
105+
spec = self.array_spec
106+
for aa_codec in aa:
107+
assert isinstance(aa_codec, SupportsSyncCodec)
108+
aa_codecs.append((aa_codec, spec))
109+
spec = aa_codec.resolve_metadata(spec)
110+
111+
self._aa_codecs = tuple(aa_codecs)
112+
assert isinstance(ab, SupportsSyncCodec)
113+
self._ab_codec = ab
114+
self._ab_spec = spec
115+
bb_sync: list[SupportsSyncCodec[Buffer, Buffer]] = []
116+
for bb_codec in bb:
117+
assert isinstance(bb_codec, SupportsSyncCodec)
118+
bb_sync.append(bb_codec)
119+
self._bb_codecs = tuple(bb_sync)
120+
121+
def decode(
122+
self,
123+
chunk_bytes: Buffer,
124+
) -> NDBuffer:
125+
"""Decode a single chunk through the full codec chain, synchronously.
126+
127+
Pure compute -- no IO.
128+
"""
129+
data: Buffer = chunk_bytes
130+
for bb_codec in reversed(self._bb_codecs):
131+
data = bb_codec._decode_sync(data, self._ab_spec)
132+
133+
chunk_array: NDBuffer = self._ab_codec._decode_sync(data, self._ab_spec)
134+
135+
for aa_codec, spec in reversed(self._aa_codecs):
136+
chunk_array = aa_codec._decode_sync(chunk_array, spec)
137+
138+
return chunk_array
139+
140+
def encode(
141+
self,
142+
chunk_array: NDBuffer,
143+
) -> Buffer | None:
144+
"""Encode a single chunk through the full codec chain, synchronously.
145+
146+
Pure compute -- no IO.
147+
"""
148+
aa_data: NDBuffer = chunk_array
149+
for aa_codec, spec in self._aa_codecs:
150+
aa_result = aa_codec._encode_sync(aa_data, spec)
151+
if aa_result is None:
152+
return None
153+
aa_data = aa_result
154+
155+
ab_result = self._ab_codec._encode_sync(aa_data, self._ab_spec)
156+
if ab_result is None:
157+
return None
158+
159+
bb_data: Buffer = ab_result
160+
for bb_codec in self._bb_codecs:
161+
bb_result = bb_codec._encode_sync(bb_data, self._ab_spec)
162+
if bb_result is None:
163+
return None
164+
bb_data = bb_result
165+
166+
return bb_data
167+
168+
def compute_encoded_size(self, byte_length: int, array_spec: ArraySpec) -> int:
169+
for codec in self.codecs:
170+
byte_length = codec.compute_encoded_size(byte_length, array_spec)
171+
array_spec = codec.resolve_metadata(array_spec)
172+
return byte_length
173+
174+
69175
@dataclass(frozen=True)
70176
class BatchedCodecPipeline(CodecPipeline):
71177
"""Default codec pipeline.

0 commit comments

Comments
 (0)