Skip to content

Commit d3c5c17

Browse files
committed
fix(testing): pass bare-int rectilinear dims through the chunks= conversion
The stateful hypothesis tests convert generated chunk grid metadata back into a create_array chunks= argument. Bare-int dimensions — the spec's step-size shorthand, now produced when a uniform dimension of a mixed grid collapses — were wrapped as single-element lists, turning "repeat to cover the axis" into "exactly one chunk" and failing the sum-to-span check (e.g. chunks=[1] for span 3). Extract the conversion into chunks_param_from_rectilinear, pass bare ints through unchanged, and widen ChunksLike to admit mixed int | sequence per-dimension specs, which the normalizer already accepted. Assisted-by: ClaudeCode:claude-fable-5
1 parent 3b277af commit d3c5c17

4 files changed

Lines changed: 48 additions & 7 deletions

File tree

‎changes/4174.bugfix.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ size + extent pair (`FixedDimension`) instead of being expanded to one entry
44
per chunk, so creating arrays like
55
`zarr.create_array(store, shape=(2**62,), chunks=(1,), dtype='int32')` succeeds
66
instantly instead of raising `ValueError` or allocating gigabytes of memory.
7-
The intermediate `ChunksTuple` representation was removed in the process.
7+
The intermediate `ChunksTuple` representation was removed in the process, and
8+
`ChunksLike` now admits per-dimension specs that mix a bare int (uniform chunk
9+
size) with explicit edge-length sequences, matching what the normalizer and
10+
the rectilinear grid spec already accepted.
811
This is the creation-time counterpart of the indexing fix in #4172.

‎src/zarr/core/common.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535

3636
BytesLike = bytes | bytearray | memoryview
3737
ShapeLike = Iterable[int | np.integer[Any]] | int | np.integer[Any]
38-
ChunksLike = ShapeLike | Iterable[Iterable[int]]
38+
# Per-dimension chunk specs may mix a bare int (uniform chunk size, the
39+
# rectilinear spec's step-size shorthand) with explicit edge-length sequences.
40+
ChunksLike = ShapeLike | Iterable[int | Iterable[int]]
3941
# For backwards compatibility
4042
ChunkCoords = tuple[int, ...]
4143
ZarrFormat = Literal[2, 3]

‎src/zarr/testing/strategies.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,17 +312,14 @@ def arrays(
312312
# - RegularChunkGridMetadata -> flat tuple of ints
313313
# - RectilinearChunkGridMetadata -> nested list of ints (triggers rectilinear path)
314314
# - v2 -> flat tuple of ints
315-
chunks_param: tuple[int, ...] | list[list[int]]
315+
chunks_param: tuple[int, ...] | list[int | list[int]]
316316
shard_shape = None
317317
dim_names = None
318318
if zarr_format == 3:
319319
chunk_grid_meta = draw(st.none() | chunk_grids(shape=nparray.shape), label="chunk grid")
320320
dim_names = draw(dimension_names(ndim=nparray.ndim), label="dimension names")
321321
if isinstance(chunk_grid_meta, RectilinearChunkGridMetadata):
322-
chunks_param = [
323-
list(dim) if isinstance(dim, tuple) else [dim]
324-
for dim in chunk_grid_meta.chunk_shapes
325-
]
322+
chunks_param = chunks_param_from_rectilinear(chunk_grid_meta)
326323
elif isinstance(chunk_grid_meta, RegularChunkGridMetadata):
327324
chunks_param = chunk_grid_meta.chunk_shape
328325
else:
@@ -404,6 +401,19 @@ def simple_arrays(
404401
)
405402

406403

404+
def chunks_param_from_rectilinear(
405+
meta: RectilinearChunkGridMetadata,
406+
) -> list[int | list[int]]:
407+
"""Convert rectilinear chunk grid metadata into a `chunks=` argument.
408+
409+
Explicit edge tuples become lists. Bare ints — the spec's step-size
410+
shorthand meaning "repeat to cover the axis" — pass through unchanged;
411+
wrapping one in a single-element list would instead declare exactly one
412+
chunk, which fails normalization whenever the axis needs more than one.
413+
"""
414+
return [list(dim) if isinstance(dim, tuple) else dim for dim in meta.chunk_shapes]
415+
416+
407417
@st.composite
408418
def rectilinear_chunks(draw: st.DrawFn, *, shape: tuple[int, ...]) -> list[list[int]]:
409419
"""Generate valid rectilinear chunk shapes for a given array shape.

‎tests/test_properties.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,3 +448,29 @@ def test_array_metadata_meets_spec(meta: ArrayV2Metadata | ArrayV3Metadata) -> N
448448
assert serialized_complex_float_is_valid(asdict_dict["fill_value"])
449449
elif dtype_native.kind in ("M", "m") and np.isnat(meta.fill_value):
450450
assert asdict_dict["fill_value"] == -9223372036854775808
451+
452+
453+
def test_chunks_param_from_rectilinear_bare_int_roundtrip() -> None:
454+
"""Bare-int dims in rectilinear metadata (the spec's step-size shorthand,
455+
produced when a uniform dimension of a mixed grid is collapsed) must pass
456+
through the `chunks=` conversion unchanged. Wrapping one in a
457+
single-element list turns "repeat to cover the axis" into "exactly one
458+
chunk" and re-creation fails the sum-to-span check."""
459+
from zarr.core.metadata.v3 import RectilinearChunkGridMetadata
460+
from zarr.storage import MemoryStore
461+
from zarr.testing.strategies import chunks_param_from_rectilinear
462+
463+
with zarr.config.set({"array.rectilinear_chunks": True}):
464+
src = zarr.create_array(
465+
MemoryStore(), shape=(3, 3), chunks=[[1, 2], [1, 1, 1]], dtype="uint8"
466+
)
467+
grid = src.metadata.chunk_grid # type: ignore[union-attr]
468+
assert isinstance(grid, RectilinearChunkGridMetadata)
469+
assert grid.chunk_shapes == ((1, 2), 1)
470+
dst = zarr.create_array(
471+
MemoryStore(),
472+
shape=src.shape,
473+
chunks=chunks_param_from_rectilinear(grid),
474+
dtype="uint8",
475+
)
476+
assert dst.metadata.chunk_grid == grid # type: ignore[union-attr]

0 commit comments

Comments
 (0)