Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
cd7d264
fix(chunk-grids): enforce one zero-length-axis invariant across model…
d-v-b Sep 9, 2026
a171507
fix(metadata): read a legacy v2 zero chunk edge on an empty axis as 1
d-v-b Sep 9, 2026
921be0d
chore: rename changelog fragment to the PR number
d-v-b Sep 9, 2026
047a92e
docs: state what 2.x actually did with a zero chunk edge
d-v-b Sep 9, 2026
7a2a71a
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Sep 12, 2026
2297c62
docs: qualify zero-chunk compatibility history
d-v-b Sep 13, 2026
2873178
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Sep 16, 2026
d868213
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Sep 16, 2026
39b63d4
test: sample the full rectilinear chunk grid declaration space
d-v-b Sep 18, 2026
0410129
docs: add changelog fragment for #4377
d-v-b Sep 18, 2026
4456d80
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Sep 19, 2026
49564dc
fix(metadata): read a legacy zero chunk size on an empty axis in Zarr…
d-v-b Sep 19, 2026
5bb1f97
refactor(metadata): check stored chunk shapes against the array shape…
d-v-b Sep 19, 2026
f93193d
refactor(metadata): scope the stored chunk shape check to regular chu…
d-v-b Sep 19, 2026
d69a6a5
test: tighten rectilinear strategy contracts from review
d-v-b Sep 24, 2026
a19718b
Merge remote-tracking branch 'upstream/main' into pr-4377
d-v-b Sep 24, 2026
f5ef3df
Merge branch 'fix/zero-length-single-invariant' (#4334) into test/rec…
d-v-b Sep 25, 2026
84b19cc
test: draw zero-length axes in the rectilinear declaration strategies
d-v-b Sep 25, 2026
64b3550
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Sep 25, 2026
8414ea3
fix(metadata): read a stored zero chunk size on a grown axis as one s…
d-v-b Sep 25, 2026
2bf32df
test: a stateful test of one array's create/append/resize/write life
d-v-b Sep 25, 2026
42230e8
Merge branch 'fix/zero-length-single-invariant' (#4334) into test/rec…
d-v-b Sep 25, 2026
1723f0b
fix(metadata): stop the zero chunk size warning from naming writers
d-v-b Sep 25, 2026
b75e1c0
Merge branch 'fix/zero-length-single-invariant' (#4334) into test/rec…
d-v-b Sep 25, 2026
3d223f1
chore(metadata): drop an orphaned comment in ArrayV2Metadata.__init__
d-v-b Sep 25, 2026
339c514
fix(chunk-grids): size a full-span shard as a multiple of the inner c…
d-v-b Sep 25, 2026
e49eb7e
refactor(metadata): read invalid stored chunk sizes in one module of …
d-v-b Sep 25, 2026
f6f5c7b
docs: rewrite the 4334 changelog fragment and trim restating docstrings
d-v-b Sep 25, 2026
f873358
test: model exactly what the store holds in the array lifecycle state…
d-v-b Sep 25, 2026
960580d
Merge loop/4334 into w4377
d-v-b Sep 25, 2026
fe4359a
test(strategies): drop the overhang parameter and trim rectilinear ev…
d-v-b Sep 25, 2026
578f935
test: check rectilinear properties against the declaration, not zarr'…
d-v-b Sep 25, 2026
9504416
test: round-trip data through a rectilinear zero-length axis as it grows
d-v-b Sep 25, 2026
60853fd
test: draw the state machine's rectilinear chunks from the shared str…
d-v-b Sep 25, 2026
d9549b8
refactor(metadata): warn about an upgraded document only once it vali…
d-v-b Sep 25, 2026
7bd21ab
Merge loop/4334 into w4377
d-v-b Sep 25, 2026
5ef0f51
refactor(metadata): one per-axis rule for stored chunk sizes, one rul…
d-v-b Sep 26, 2026
192f86c
refactor(chunk-grids): start chunk guessing from the one full-span rule
d-v-b Sep 26, 2026
2e878cf
test: run the array lifecycle state machine in the slow Hypothesis job
d-v-b Sep 26, 2026
17d6686
Merge loop/4334 into w4377
d-v-b Sep 26, 2026
986576b
test: draw distinct rectilinear dividers without rejection
d-v-b Sep 26, 2026
c571b2a
refactor(testing): type rectilinear declarations with RectilinearDimS…
d-v-b Sep 26, 2026
0cc909e
test: draw the list form of rectilinear chunks= from rectilinear_chunks
d-v-b Sep 26, 2026
c0d4b3c
fix(array): store upgraded metadata before writing the first chunk
d-v-b Sep 26, 2026
121b6b2
refactor(metadata): name an array one way in upgrade warnings
d-v-b Sep 26, 2026
c1353bf
refactor(metadata): one rule for bare sizes and edge lists; one noun …
d-v-b Sep 26, 2026
1b14c10
Merge branch 'loop/4334' into loop/4377
d-v-b Sep 26, 2026
0d640f8
refactor(testing)!: remove chunks_param_from_rectilinear
d-v-b Sep 26, 2026
84c85a6
refactor(testing): drop the extent-1 special case in rectilinear_dim_…
d-v-b Sep 26, 2026
ab7d03b
perf(testing): bound the chunks a drawn rectilinear grid declares in …
d-v-b Sep 26, 2026
f081f1b
fix(array): store the upgrade of the current stored document before w…
d-v-b Sep 26, 2026
9e29333
test: prefer zero-length axes for stored chunk size 0 in the lifecycl…
d-v-b Sep 26, 2026
396558a
Merge loop/4334 into w4377
d-v-b Sep 26, 2026
d7a9696
refactor(testing): declare the rectilinear chunk limits and per-axis …
d-v-b Sep 26, 2026
632ac2b
fix(metadata): keep accepting the chunk sizes zarr 3.4.0 accepted in …
d-v-b Sep 26, 2026
245acae
fix(array): leave a valid stored document as written on a stale handl…
d-v-b Sep 26, 2026
2d0121d
docs: describe the 4334 changes to metadata constructors for a patch …
d-v-b Sep 26, 2026
82a132d
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
0ed0015
fix(testing): keep chunks_param_from_rectilinear in zarr.testing (pat…
d-v-b Sep 26, 2026
6caf00e
fix(testing): keep rectilinear_chunks drawing an edge list per dimens…
d-v-b Sep 26, 2026
f7c10df
docs: describe the 4377 strategy changes as additive for a patch release
d-v-b Sep 26, 2026
4c4fe21
fix(metadata): read float edges only in stored rectilinear documents
d-v-b Sep 26, 2026
5862c18
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
6fde402
test(codecs): pickle a ShardingCodec with an inner chunk size of 0
d-v-b Sep 26, 2026
b0ea125
fix(group): build every node before create_hierarchy deletes or store…
d-v-b Sep 26, 2026
a903d66
fix(metadata): warn only where the user must act; guard and refresh u…
d-v-b Sep 26, 2026
f78e115
test: expect the lifecycle machine's upgrade warning only on non-empt…
d-v-b Sep 26, 2026
93a7e78
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
ceb31fd
test: pin chunks_param_from_rectilinear returning lists, as zarr 3.4.…
d-v-b Sep 26, 2026
bc306aa
fix(metadata): leave a sharded chunk size of 0 unread when the inner …
d-v-b Sep 26, 2026
3bc802f
perf(metadata): read each upgraded member once, concurrently, and ado…
d-v-b Sep 26, 2026
04c5075
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
dc50ec6
fix(group): adopt refreshed consolidated members in place
d-v-b Sep 26, 2026
cddb877
fix(metadata): raise the rectilinear flag error when refreshing a con…
d-v-b Sep 26, 2026
958b31c
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
4b9f82c
fix(group): store a group's documents only; keep upgraded members as …
d-v-b Sep 26, 2026
e00178f
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
65f3f8e
docs(group): say what consolidated metadata stores for an upgraded array
d-v-b Sep 26, 2026
58cfe28
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
faa6efe
fix(array): clear the stored document whenever an array stores its me…
d-v-b Sep 26, 2026
e9e45bd
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
30d7ca6
test(metadata): pin that a failed metadata save keeps the stored docu…
d-v-b Sep 26, 2026
d13c317
Merge loop/4334 into loop/4377
d-v-b Sep 26, 2026
7a0154e
Merge remote-tracking branch 'up/main' into up/4334
d-v-b Sep 29, 2026
b8c6c5b
Merge branch 'up/4334' into up/4377
d-v-b Sep 29, 2026
6707b45
fix(metadata): read a stored chunk size of 0 as 1, however long the axis
d-v-b Sep 29, 2026
d43470f
Merge branch 'up/4334' into up/4377
d-v-b Sep 29, 2026
9e07aaf
fix(metadata): re-save only upgrades that move chunks, and read non-J…
d-v-b Sep 29, 2026
8f1320b
Merge branch 'up/4334' into up/4377
d-v-b Sep 29, 2026
9dd674f
Merge branch 'main' into test/rectilinear-declaration-space
d-v-b Sep 30, 2026
5dccfb8
fix(metadata): recommend recreating an array whose stored chunk size …
d-v-b Sep 30, 2026
583dfaa
Merge branch 'work/4334' into work/4377
d-v-b Sep 30, 2026
fcc24bb
fix(metadata): give the recipe for recreating an array read from a st…
d-v-b Sep 30, 2026
7249366
Merge branch 'work/4334' into work/4377
d-v-b Sep 30, 2026
6c77b90
Merge branch 'main' into test/rectilinear-declaration-space
d-v-b Sep 30, 2026
c4eb475
Merge branch 'main' into fix/zero-length-single-invariant
d-v-b Oct 1, 2026
fbb0bbf
Merge commit 'c4eb4757e' into HEAD
d-v-b Oct 1, 2026
0992e94
refactor(metadata): call the lenient readings of stored documents rep…
d-v-b Oct 1, 2026
d778da3
Merge commit '0992e948b' into HEAD
d-v-b Oct 1, 2026
2976502
Merge branch 'main' into test/rectilinear-declaration-space
d-v-b Oct 1, 2026
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
1 change: 1 addition & 0 deletions changes/4377.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The hypothesis strategies in `zarr.testing.strategies` now sample the full space of rectilinear chunk grid declarations: `arrays()` passes a rectilinear grid to `create_array` either as metadata or as a `chunks=` specification mixing bare-int steps and explicit edge lists in any arrangement, and the new `rectilinear_chunk_shape_declarations` and `rectilinear_chunk_grids` cover stored metadata, including run-length encoded edge lists in canonical and arbitrary groupings and edges overhanging the array extent. `rectilinear_chunks` and `chunk_grids` also draw zero-length dimensions.
271 changes: 205 additions & 66 deletions src/zarr/testing/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
from zarr.core.dtype.npy.structured import Struct
from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar, ZDType
from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata
from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata
from zarr.core.metadata.v3 import (
RectilinearChunkGridMetadata,
RectilinearDimSpecJSON,
RegularChunkGridMetadata,
)
from zarr.core.sync import sync
from zarr.storage import MemoryStore, StoreLike
from zarr.storage._utils import _join_paths, normalize_path
Expand Down Expand Up @@ -444,18 +448,27 @@ def arrays(
array_path = _join_paths([path, name])
root = zarr.open_group(store, mode=open_mode, zarr_format=zarr_format)

# Convert chunk grid metadata to a form create_array accepts:
# - RegularChunkGridMetadata -> flat tuple of ints
# - RectilinearChunkGridMetadata -> nested list of ints (triggers rectilinear path)
# - v2 -> flat tuple of ints
chunks_param: tuple[int, ...] | list[int | list[int]]
chunks_param: tuple[int, ...] | list[int | list[int]] | RectilinearChunkGridMetadata
shard_shape = None
dim_names = None
if zarr_format == 3:
chunk_grid_meta = draw(st.none() | chunk_grids(shape=nparray.shape), label="chunk grid")
dim_names = draw(dimension_names(ndim=nparray.ndim), label="dimension names")
if isinstance(chunk_grid_meta, RectilinearChunkGridMetadata):
chunks_param = chunks_param_from_rectilinear(chunk_grid_meta)
# A rectilinear grid is passed either as the metadata object or in
# the list form of `chunks=`, drawn from its own strategy. A 0-d
# array has no dimension to hold an edge list.
if nparray.ndim > 0 and draw(st.booleans(), label="chunks as lists"):
event("rectilinear chunks= as lists")
chunks_param = draw(_rectilinear_chunks(shape=nparray.shape), label="chunks")
chunk_grid_meta = RectilinearChunkGridMetadata(
chunk_shapes=tuple(
dim if isinstance(dim, int) else tuple(dim) for dim in chunks_param
)
)
else:
event("rectilinear chunks= as metadata")
chunks_param = chunk_grid_meta
elif isinstance(chunk_grid_meta, RegularChunkGridMetadata):
chunks_param = chunk_grid_meta.chunk_shape
else:
Expand Down Expand Up @@ -505,7 +518,9 @@ def arrays(
)
assert shard_shape == a.shards
else:
assert isinstance(a.metadata.chunk_grid, RectilinearChunkGridMetadata)
# The stored grid is exactly the declared one: bare ints stay bare
# ints, edge lists keep their edges.
assert a.metadata.chunk_grid == chunk_grid_meta
assert shard_shape is None

assert a.basename == name, (a.basename, name)
Expand Down Expand Up @@ -546,59 +561,190 @@ def chunks_param_from_rectilinear(
return [list(dim) if isinstance(dim, tuple) else dim for dim in meta.chunk_shapes]


# The most chunks a drawn rectilinear grid declares along one axis, and over
# the array, all axes together. Indexing tests visit every chunk, so the
# product is what costs time.
_RECTILINEAR_MAX_CHUNKS_PER_DIM = 20
_RECTILINEAR_CHUNK_BUDGET = 400


def _max_chunks_per_dim(ndim: int) -> int:
"""At most `_RECTILINEAR_MAX_CHUNKS_PER_DIM` chunks per dimension, and at
most `_RECTILINEAR_CHUNK_BUDGET` in total over `ndim` dimensions."""
return max(
k
for k in range(1, _RECTILINEAR_MAX_CHUNKS_PER_DIM + 1)
if k**ndim <= _RECTILINEAR_CHUNK_BUDGET
)


@st.composite
def rectilinear_dim_edges(
draw: st.DrawFn, *, extent: int, max_chunks: int = _RECTILINEAR_MAX_CHUNKS_PER_DIM
) -> list[int]:
"""Explicit chunk edge lengths summing exactly to `extent`.

A zero `extent` has no chunks for edges to cover, and no non-empty list
of positive edges sums to 0; its edges are the chunks the axis grows into
on `append` or `resize`, so any non-empty list of positive edges is drawn.

Two modes: "uneven" cuts the extent at random dividers; "uniform" repeats
one size with an optional remainder, optionally shuffled so equal edges
are not all adjacent. At most `max_chunks` chunks keeps property tests
fast.
"""
assert extent >= 0
if extent == 0:
event("rectilinear edges: zero extent")
return draw(st.lists(st.integers(min_value=1, max_value=10), min_size=1, max_size=5))
if draw(st.booleans(), label="uneven edges"):
nchunks = draw(st.integers(min_value=1, max_value=min(extent, max_chunks)))
# Draw distinct dividers by index into the unused positions: no
# rejection, unlike `st.lists(..., unique=True)`.
positions = list(range(1, extent))
dividers = sorted(
positions.pop(draw(st.integers(min_value=0, max_value=len(positions) - 1)))
for _ in range(nchunks - 1)
)
return [b - a for a, b in zip([0, *dividers], [*dividers, extent], strict=True)]
size = draw(st.integers(min_value=math.ceil(extent / max_chunks), max_value=extent))
edges = [size] * (extent // size)
if extent % size:
edges.append(extent % size)
if draw(st.booleans(), label="shuffle uniform edges"):
return list(draw(st.permutations(edges)))
return edges


def _rectilinear_step(draw: st.DrawFn, *, extent: int, max_chunks: int) -> int:
"""A bare-int chunk size for one dimension: a step that repeats to cover
the extent, with the last chunk possibly smaller. A step larger than the
extent (one overhanging chunk) is allowed, as for a regular grid."""
step = draw(st.integers(min_value=max(1, math.ceil(extent / max_chunks)), max_value=extent + 3))
if step > extent:
event("rectilinear step: larger than extent")
return step


@st.composite
def rectilinear_chunks(draw: st.DrawFn, *, shape: tuple[int, ...]) -> list[list[int]]:
"""Generate valid rectilinear chunk shapes for a given array shape.
"""A `chunks=` specification declaring a rectilinear grid over `shape`, as an
explicit edge list per dimension summing to the extent (any edges, for a zero
extent). A 0-d `shape` gives `[]`.

To also draw bare-int steps, see `_rectilinear_chunks`.
"""
max_chunks = _max_chunks_per_dim(len(shape))
return [draw(rectilinear_dim_edges(extent=e, max_chunks=max_chunks)) for e in shape]

Uses two modes per dimension:
- "expanded": random divider points create arbitrary chunk sizes
- "rle": uniform chunks with optional remainder, optionally shuffled

Keeps max chunks per dimension <= 20 to avoid performance issues
in property tests. With higher dimensions, the total chunk count
grows multiplicatively.
@st.composite
def _rectilinear_chunks(draw: st.DrawFn, *, shape: tuple[int, ...]) -> list[int | list[int]]:
"""A `chunks=` specification declaring a rectilinear grid over `shape`.

Each dimension is either a bare int (a step size; the last chunk may be
smaller) or an explicit edge list summing to the extent (any edges, for a
zero extent). At least one dimension is an edge list, since bare ints
alone declare a regular grid.
Run-length encoding is not part of the `chunks=` syntax; it belongs to
stored metadata, see `rectilinear_chunk_shape_declarations`.

`shape` must have at least one dimension: a 0-d array has no dimension
to hold an edge list, so it cannot have a rectilinear grid.
"""
chunk_shapes: list[list[int]] = []
for size in shape:
assert size > 0
if size > 1:
mode = draw(st.sampled_from(["expanded", "rle"]))
if mode == "expanded":
event("rectilinear expanded")
max_chunks = min(size - 1, 20)
nchunks = draw(st.integers(min_value=1, max_value=max_chunks))
dividers = sorted(
draw(
st.lists(
st.integers(min_value=1, max_value=size - 1),
min_size=nchunks - 1,
max_size=nchunks - 1,
unique=True,
)
)
)
chunk_shapes.append(
[a - b for a, b in zip(dividers + [size], [0] + dividers, strict=False)]
)
else:
# RLE mode: uniform chunks with optional remainder
max_chunk_size = min(size, 20)
chunk_size = draw(st.integers(min_value=1, max_value=max_chunk_size))
n_full = size // chunk_size
remainder = size % chunk_size
chunks_list = [chunk_size] * n_full
if remainder > 0:
chunks_list.append(remainder)
# Optionally shuffle to create non-contiguous duplicate patterns
if draw(st.booleans()):
event("rectilinear rle shuffled")
chunks_list = draw(st.permutations(chunks_list))
else:
event("rectilinear rle")
chunk_shapes.append(list(chunks_list))
assert shape, "a rectilinear grid needs at least one dimension"
max_chunks = _max_chunks_per_dim(len(shape))
forced_list = draw(st.integers(min_value=0, max_value=len(shape) - 1))
chunks: list[int | list[int]] = []
for i, extent in enumerate(shape):
if i != forced_list and draw(st.booleans(), label="bare int"):
chunks.append(_rectilinear_step(draw, extent=extent, max_chunks=max_chunks))
else:
chunk_shapes.append([1])
return chunk_shapes
chunks.append(draw(rectilinear_dim_edges(extent=extent, max_chunks=max_chunks)))
return chunks


def _rle_encode(draw: st.DrawFn, edges: list[int]) -> list[int | list[int]]:
"""Run-length encode `edges` as the spec allows: a mix of bare ints and
`[size, count]` pairs. Either the canonical form (each run as one pair,
a run of one as a bare int) or an arbitrary grouping, which may split a
run across pairs and use `count == 1`."""
canonical = draw(st.booleans(), label="canonical rle")
if not canonical:
event("rectilinear rle: arbitrary grouping")
encoded: list[int | list[int]] = []
i = 0
while i < len(edges):
run = 1
while i + run < len(edges) and edges[i + run] == edges[i]:
run += 1
if canonical:
count = run
bare = run == 1
else:
count = draw(st.integers(min_value=1, max_value=run))
bare = count == 1 and draw(st.booleans(), label="bare edge")
encoded.append(edges[i] if bare else [edges[i], count])
i += count
return encoded


def _rectilinear_chunk_shapes(
draw: st.DrawFn, shape: tuple[int, ...]
) -> tuple[int | tuple[int, ...], ...]:
"""The `chunk_shapes` of a stored rectilinear grid over `shape`: per
dimension a bare-int step, or explicit edges. Edges may sum beyond the
extent, which the spec allows and a shrinking resize produces."""
max_chunks = _max_chunks_per_dim(len(shape))
chunk_shapes: list[int | tuple[int, ...]] = []
for extent in shape:
if draw(st.booleans(), label="bare int"):
chunk_shapes.append(_rectilinear_step(draw, extent=extent, max_chunks=max_chunks))
continue
edges = draw(rectilinear_dim_edges(extent=extent, max_chunks=max_chunks))
if extent > 0 and draw(st.booleans(), label="overhang"):
event("rectilinear edges: overhang")
if draw(st.booleans(), label="trailing edge"):
edges = [*edges, draw(st.integers(min_value=1, max_value=5))]
else:
edges[-1] += draw(st.integers(min_value=1, max_value=5))
chunk_shapes.append(tuple(edges))
return tuple(chunk_shapes)


@st.composite
def rectilinear_chunk_shape_declarations(
draw: st.DrawFn, *, shape: tuple[int, ...]
) -> tuple[list[RectilinearDimSpecJSON], tuple[int | tuple[int, ...], ...]]:
"""The `chunk_shapes` of a stored rectilinear chunk grid, with its meaning.

Samples the whole declaration space of the spec. Per dimension: a bare
int step, or an edge list written in full or run-length encoded
(canonically, or with arbitrary grouping). Edge lists may sum beyond the
extent.

Returns `(declaration, chunk_shapes)`: the JSON value to store, and the
`chunk_shapes` that parsing it must produce.
"""
chunk_shapes = _rectilinear_chunk_shapes(draw, shape)
declaration: list[RectilinearDimSpecJSON] = [
dim
if isinstance(dim, int)
else list(dim)
if draw(st.booleans(), label="write edges in full")
else _rle_encode(draw, list(dim))
for dim in chunk_shapes
]
return declaration, chunk_shapes


@st.composite
def rectilinear_chunk_grids(
draw: st.DrawFn, *, shape: tuple[int, ...]
) -> RectilinearChunkGridMetadata:
"""A `RectilinearChunkGridMetadata` over `shape`, per dimension a bare-int
step or explicit edges, which may sum beyond the extent."""
return RectilinearChunkGridMetadata(chunk_shapes=_rectilinear_chunk_shapes(draw, shape))


@st.composite
Expand All @@ -613,16 +759,9 @@ def chunk_grids(

This allows property tests to exercise both chunk grid types.
"""
# RectilinearChunkGridMetadata doesn't support zero-sized dimensions,
# so use RegularChunkGridMetadata if any dimension is 0
if any(s == 0 for s in shape):
event("using RegularChunkGridMetadata (zero-sized dimensions)")
return RegularChunkGridMetadata(chunk_shape=draw(chunk_shapes(shape=shape)))

if zarr.config.get("array.rectilinear_chunks") and draw(st.booleans()):
chunks = draw(rectilinear_chunks(shape=shape))
event("using RectilinearChunkGridMetadata")
return RectilinearChunkGridMetadata(chunk_shapes=tuple(tuple(dim) for dim in chunks))
return draw(rectilinear_chunk_grids(shape=shape))
else:
event("using RegularChunkGridMetadata")
return RegularChunkGridMetadata(chunk_shape=draw(chunk_shapes(shape=shape)))
Expand All @@ -640,7 +779,7 @@ def rectilinear_arrays(
) -> Any:
"""Generate a zarr v3 array with rectilinear (variable) chunk grid."""
shape = draw(shapes)
chunk_shapes = draw(rectilinear_chunks(shape=shape))
chunk_shapes = draw(_rectilinear_chunks(shape=shape))

np_dtype = draw(dtypes())
nparray = draw(numpy_arrays(shapes=st.just(shape), dtype=np_dtype))
Expand Down Expand Up @@ -943,7 +1082,7 @@ def block_test_arrays(
``zarray.write_chunk_sizes`` — the array's *outer* (block / shard) grid, which
is exactly the grid ``Array.blocks`` addresses; the caller reads it directly.
"""
chunks: tuple[int, ...] | list[list[int]]
chunks: tuple[int, ...] | list[int | list[int]]
if draw(st.booleans()):
# regular arm, optionally sharded
nparray, chunks = draw(
Expand All @@ -960,7 +1099,7 @@ def block_test_arrays(
# rectilinear arm, always unsharded
event("block rectilinear")
shape = draw(_rectilinear_shapes)
chunks = draw(rectilinear_chunks(shape=shape))
chunks = draw(_rectilinear_chunks(shape=shape))
nparray = draw(numpy_arrays(shapes=st.just(shape), dtype=draw(dtypes())))
shards, rectilinear = None, True

Expand Down
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import itertools
import math
import os
import pathlib
Expand Down Expand Up @@ -574,6 +575,21 @@ def deep_nan_equal(a: object, b: object) -> bool:
return nan_equal(a, b)


def declared_chunk_data_sizes(declared: int | Sequence[int], extent: int) -> tuple[int, ...]:
"""The data sizes of the chunks one declared chunk grid dimension places
over `extent`, worked out from the declaration alone: a bare int repeats
to cover the extent, explicit edges are clipped to it. An oracle that does
not go through zarr's chunk grid code."""
sizes: list[int] = []
offset = 0
for edge in itertools.repeat(declared) if isinstance(declared, int) else declared:
if offset >= extent:
break
sizes.append(min(edge, extent - offset))
offset += edge
return tuple(sizes)


def gzip_streams_equal_except_mtime(a: bytes, b: bytes) -> bool:
"""Compare two gzip streams, ignoring the MTIME field of the header.

Expand Down
Loading
Loading