Skip to content

Commit a50d791

Browse files
d-v-bclaude
andauthored
fix(sharding): flatten any multi-dim value for coordinate selections in partial writes (zarr-developers#4316)
* fix(sharding): flatten any multi-dim value for coordinate selections in partial writes The guard added in zarr-developers#4284 only reshaped the value when its shape equalled the re-derived CoordinateIndexer's sel_shape. An orthogonal selection that mixes an integer index with two or more array indices defeats that: OrthogonalIndexer drops the integer axis from the value but np.ix_ keeps it as a length-1 axis in the chunk selection, so the shapes differ in rank while agreeing in element count, the reshape was skipped, and the write still raised the shape-mismatch ValueError. The invariant is that a coordinate indexer addresses the value flat, so ravel any multi-dimensional value instead. Both partial-encode paths now share one helper for deriving the shard indexer and shaping the value, and the check is an isinstance on CoordinateIndexer so mypy types sel_shape. The regression test is parametrized over selections with an integer axis in each position, three array axes, and an unsorted selection spanning two shards. Closes zarr-developers#4315 Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: add changelog entry for #320 Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Rename 320.bugfix.md to 4316.bugfix.md * fix(sharding): ravel only a value shaped like the selection minus its unit axes Ravelling every multi-dimensional value for a coordinate selection was too lenient. A mask write with a (2, 2) value for four selected elements, or an orthogonal write with a spurious trailing axis, raises on an unsharded array but was silently accepted on a sharded one, because the element count matched and the shard-level selection cannot tell orthogonal from mask indexing. The value shape can. An np.ix_ selection has an N-D sel_shape and the caller's value is that shape minus the integer-indexed axes, which np.ix_ keeps as length-1 axes. Ravel exactly that shape and leave any other rank alone, so an invalid write fails the same way it does without sharding. Adds an error test for both leniencies and a positive case with a length-1 array axis next to an integer axis. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: exercise sharded writes and bare integer axes in the indexing property tests The property tests could not have found the sharded orthogonal-write bugs: - test_oindex, test_mask_indexing and test_block_indexing skipped their set half on sharded arrays with assume(zarray.shards is None), added in zarr-developers#2825 when the bug was first seen and never lifted. test_vindex had its set half commented out. - orthogonal_indices wrapped every bare integer as a one-element array, so zarr never received an integer index and OrthogonalIndexer's dropped-axis path was unreachable. basic_indices(min_dims=1) never yields an integer either, so that branch was dead. - arrays() only drew a shard shape when every axis had a chunk strictly between 1 and the axis length, on top of the v3 and regular-grid draws: 2 of 500 test_oindex examples were sharded. Lift the skips, draw integers explicitly and give the numpy indexer the same dropped-axis result, enable the vindex write with a duplicate-point filter, and let any chunk that fits the array be sharded (33 of 500 now). With these changes test_oindex fails against the code before this PR with the mixed-integer shape mismatch, and passes with it. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test: assert invalid value ranks are rejected on chunked and sharded arrays alike Replace the sharded-only wrong-rank test in test_sharding.py and the GH2469 one-off in test_indexing.py with one parametrized error test: a coordinate write with twice the elements, a mask write with a 2-D value, and an orthogonal write with an extra axis each raise ValueError on chunked and sharded arrays under both codec pipelines. The property under test is that storage layout does not change which writes are rejected, which a sharded-only test could not state. zarr_array_from_numpy_array grows a shards argument for it. Only the rejection is asserted; a write that fails inside the chunk merge may already have touched other chunks on a chunked array. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 9c29a0d commit a50d791

6 files changed

Lines changed: 196 additions & 86 deletions

File tree

changes/4316.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed a `ValueError` when setting an orthogonal selection on a sharded array that mixes an integer index with two or more array indices, such as `a.oindex[[3, 1, 2], 1, [0, 2]] = value`. The fix for the array-only case in #4284 reshaped the value only when its shape matched the coordinate selection exactly; the sharding codec now also ravels a value that is the selection's shape minus the integer-indexed axes. Values of any other rank are left alone, so a write that is invalid on an unsharded array fails the same way on a sharded one. Both partial-encode paths share one helper for this.

src/zarr/codecs/sharding.py

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from zarr.core.indexing import (
5858
BasicIndexer,
5959
ChunkProjection,
60+
CoordinateIndexer,
6061
SelectorTuple,
6162
SliceDimIndexer,
6263
_lexicographic_order,
@@ -399,6 +400,25 @@ def to_dict_vectorized(self) -> dict[tuple[int, ...], Buffer | None]:
399400
return result
400401

401402

403+
def _drops_only_unit_axes(shape: tuple[int, ...], full: tuple[int, ...]) -> bool:
404+
"""Return whether ``shape`` is ``full`` with zero or more length-1 axes removed.
405+
406+
``(2, 2)`` is ``(2, 1, 2)`` minus its unit axis, and ``(1, 2)`` is
407+
``(1, 2, 1)`` minus its last; ``(2, 2)`` is not ``(4,)``, and
408+
``(3, 2, 1)`` is not ``(3, 2)`` because it adds an axis.
409+
"""
410+
remaining = iter(full)
411+
for size in shape:
412+
for full_size in remaining:
413+
if full_size == size:
414+
break
415+
if full_size != 1:
416+
return False
417+
else:
418+
return False
419+
return all(full_size == 1 for full_size in remaining)
420+
421+
402422
@dataclass(frozen=True)
403423
class ShardingCodec(
404424
ArrayBytesCodec, ArrayBytesCodecPartialDecodeMixin, ArrayBytesCodecPartialEncodeMixin
@@ -791,23 +811,11 @@ def _encode_partial_sync(
791811
Loads the existing shard, merges the written region into the affected
792812
inner chunks, and rewrites the whole shard.
793813
"""
794-
shard_shape = shard_spec.shape
795814
chunks_per_shard = self._get_chunks_per_shard(shard_spec)
796815
chunk_spec = self._get_chunk_spec(shard_spec)
797816
inner_transform = self._get_inner_chunk_transform(shard_spec)
798817

799-
shard_indexer = get_indexer(
800-
selection,
801-
shape=shard_shape,
802-
chunk_grid=ChunkGrid.from_sizes(shard_shape, self.chunk_shape),
803-
)
804-
# A coordinate indexer flattens the selection, so its projections address
805-
# `value` as 1-D while the caller shaped it like `sel_shape`. Mirrors the
806-
# reshape `_encode_partial_single` applies on the async path.
807-
sel_shape = getattr(shard_indexer, "sel_shape", None)
808-
if sel_shape is not None and value.shape == sel_shape:
809-
value = value.reshape(shard_indexer.shape)
810-
indexer = list(shard_indexer)
818+
indexer, value = self._get_shard_indexer_and_value(selection, shard_spec, value)
811819

812820
is_complete = self._is_complete_shard_write(indexer, chunks_per_shard)
813821

@@ -1359,23 +1367,10 @@ async def _encode_partial_single(
13591367
selection: SelectorTuple,
13601368
shard_spec: ArraySpec,
13611369
) -> None:
1362-
shard_shape = shard_spec.shape
1363-
chunk_shape = self.chunk_shape
13641370
chunks_per_shard = self._get_chunks_per_shard(shard_spec)
13651371
chunk_spec = self._get_chunk_spec(shard_spec)
13661372

1367-
shard_indexer = get_indexer(
1368-
selection,
1369-
shape=shard_shape,
1370-
chunk_grid=ChunkGrid.from_sizes(shard_shape, chunk_shape),
1371-
)
1372-
# A coordinate indexer flattens the selection, so its projections address
1373-
# `shard_array` as 1-D while the caller shaped it like `sel_shape`. This
1374-
# mirrors the reshape `_decode_partial_single` applies on the way out.
1375-
sel_shape = getattr(shard_indexer, "sel_shape", None)
1376-
if sel_shape is not None and shard_array.shape == sel_shape:
1377-
shard_array = shard_array.reshape(shard_indexer.shape)
1378-
indexer = list(shard_indexer)
1373+
indexer, shard_array = self._get_shard_indexer_and_value(selection, shard_spec, shard_array)
13791374

13801375
if self._is_complete_shard_write(indexer, chunks_per_shard):
13811376
shard_dict = dict.fromkeys(lexicographic_order_coords(chunks_per_shard))
@@ -1433,6 +1428,45 @@ async def _encode_shard_dict(
14331428
index_bytes, buffers, buffer_prototype, chunks_per_shard=chunks_per_shard
14341429
)
14351430

1431+
def _get_shard_indexer_and_value(
1432+
self, selection: SelectorTuple, shard_spec: ArraySpec, value: NDBuffer
1433+
) -> tuple[list[ChunkProjection], NDBuffer]:
1434+
"""Index ``selection`` over the inner chunk grid, flattening ``value`` to match.
1435+
1436+
``get_indexer`` classifies a tuple of integer arrays as a coordinate
1437+
selection, and a ``CoordinateIndexer`` addresses the value buffer as
1438+
1-D. An ``OrthogonalIndexer`` with two or more array-indexed axes hands
1439+
down an ``np.ix_`` tuple, so ``sel_shape`` is N-D, while the caller
1440+
shaped ``value`` like the orthogonal result: the broadcast shape minus
1441+
the integer-indexed axes, which ``np.ix_`` keeps as length-1 axes. That
1442+
value has the element count and C order of the flattened projections,
1443+
so ravel it.
1444+
1445+
Only that value shape is ravelled. A mask or coordinate selection
1446+
arrives with a 1-D ``sel_shape`` and a value that must already be flat,
1447+
and an orthogonal value with an axis the selection does not have is
1448+
invalid. Both are left alone so the write fails the same way it does
1449+
on an unsharded array; the shard-level selection alone cannot tell
1450+
orthogonal from mask indexing, the value shape can. Scalars pass
1451+
through and are broadcast downstream.
1452+
1453+
The partial-decode paths apply the inverse reshape, to
1454+
``indexer.sel_shape``, on the way out.
1455+
"""
1456+
shard_shape = shard_spec.shape
1457+
indexer = get_indexer(
1458+
selection,
1459+
shape=shard_shape,
1460+
chunk_grid=ChunkGrid.from_sizes(shard_shape, self.chunk_shape),
1461+
)
1462+
if (
1463+
isinstance(indexer, CoordinateIndexer)
1464+
and len(value.shape) > 1
1465+
and _drops_only_unit_axes(value.shape, indexer.sel_shape)
1466+
):
1467+
value = value.reshape(indexer.shape)
1468+
return list(indexer), value
1469+
14361470
def _is_total_shard(
14371471
self, all_chunk_coords: set[tuple[int, ...]], chunks_per_shard: tuple[int, ...]
14381472
) -> bool:

src/zarr/testing/strategies.py

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -328,11 +328,14 @@ def arrays(
328328
else:
329329
chunks_param = draw(chunk_shapes(shape=nparray.shape), label="chunk shape")
330330

331-
if all(s > c > 1 for s, c in zip(nparray.shape, chunks_param, strict=True)):
331+
# Any chunk that fits the array can be sharded: shard_shapes draws a
332+
# whole number of chunks per axis, one inner chunk included.
333+
if all(s >= c >= 1 for s, c in zip(nparray.shape, chunks_param, strict=True)):
332334
shard_shape = draw(
333335
st.none() | shard_shapes(shape=nparray.shape, chunk_shape=chunks_param),
334336
label="shard shape",
335337
)
338+
event("sharded" if shard_shape is not None else "unsharded")
336339
if shard_shape is not None:
337340
subchunk_write_order = draw(subchunk_write_orders)
338341
inner_codecs = draw(sharding_inner_codecs, label="sharding inner codecs")
@@ -572,20 +575,30 @@ def basic_indices(
572575
@st.composite
573576
def orthogonal_indices(
574577
draw: st.DrawFn, *, shape: tuple[int, ...]
575-
) -> tuple[tuple[np.ndarray[Any, Any], ...], tuple[np.ndarray[Any, Any], ...]]:
578+
) -> tuple[tuple[int | slice | np.ndarray[Any, Any], ...], tuple[np.ndarray[Any, Any], ...]]:
576579
"""
577580
Strategy that returns
578-
(1) a tuple of integer arrays used for orthogonal indexing of Zarr arrays.
579-
(2) a tuple of integer arrays that can be used for equivalent indexing of numpy arrays
581+
(1) a tuple of per-axis selectors (integer array, slice, or bare integer) for
582+
orthogonal indexing of Zarr arrays.
583+
(2) a tuple of broadcast integer arrays that index a numpy array to the same
584+
result. A bare integer drops its axis, as ``oindex`` does, so it is
585+
given as a 0-d array and does not contribute a result dimension.
580586
"""
581-
zindexer = []
582-
npindexer = []
583-
ndim = len(shape)
587+
zindexer: list[int | slice | np.ndarray[Any, Any]] = []
588+
kept: list[tuple[int, np.ndarray[Any, Any]]] = []
589+
npindexer: dict[int, np.ndarray[Any, Any]] = {}
584590
for axis, size in enumerate(shape):
585591
if size != 0:
586-
strategy = npst.integer_array_indices(
587-
shape=(size,), result_shape=npst.array_shapes(min_side=1, max_side=size, max_dims=1)
588-
) | basic_indices(min_dims=1, shape=(size,), allow_ellipsis=False)
592+
strategy = (
593+
npst.integer_array_indices(
594+
shape=(size,),
595+
result_shape=npst.array_shapes(min_side=1, max_side=size, max_dims=1),
596+
)
597+
| basic_indices(min_dims=1, shape=(size,), allow_ellipsis=False)
598+
# basic_indices(min_dims=1) never yields a bare integer, so draw
599+
# one explicitly: it is the only selector that drops an axis.
600+
| st.integers(min_value=-size, max_value=size - 1)
601+
)
589602
else:
590603
strategy = basic_indices(min_dims=1, shape=(size,), allow_ellipsis=False)
591604

@@ -597,19 +610,25 @@ def orthogonal_indices(
597610
.filter(bool)
598611
)
599612
(idxr,) = val
600-
if isinstance(idxr, int):
601-
idxr = np.array([idxr])
602613
zindexer.append(idxr)
614+
if isinstance(idxr, int):
615+
npindexer[axis] = np.array(idxr)
616+
continue
603617
if isinstance(idxr, slice):
604618
idxr = np.arange(*idxr.indices(size))
605-
elif isinstance(idxr, (tuple, int)):
619+
elif isinstance(idxr, tuple):
606620
idxr = np.array(idxr)
607-
newshape = [1] * ndim
608-
newshape[axis] = idxr.size
609-
npindexer.append(idxr.reshape(newshape))
621+
kept.append((axis, idxr))
622+
623+
for pos, (axis, idxr) in enumerate(kept):
624+
newshape = [1] * len(kept)
625+
newshape[pos] = idxr.size
626+
npindexer[axis] = idxr.reshape(newshape)
610627

611628
# casting the output of broadcast_arrays is needed for numpy < 2
612-
return tuple(zindexer), tuple(np.broadcast_arrays(*npindexer))
629+
return tuple(zindexer), tuple(
630+
np.broadcast_arrays(*(npindexer[axis] for axis in range(len(shape))))
631+
)
613632

614633

615634
@st.composite

tests/test_codecs/test_sharding.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
import math
23
import pickle
34
import warnings
45
from typing import Any, cast, get_args
@@ -1272,31 +1273,58 @@ def test_shard_reader_to_dict_vectorized(chunks_per_shard: tuple[int, ...]) -> N
12721273
],
12731274
)
12741275
@pytest.mark.parametrize("nested", [False, True], ids=["single", "nested"])
1275-
def test_sharding_orthogonal_set_multiple_array_dims(nested: bool, pipeline_path: str) -> None:
1276+
@pytest.mark.parametrize(
1277+
"selection",
1278+
[
1279+
pytest.param((np.array([3, 1, 2]), np.array([0, 2])), id="2d-arr-arr"),
1280+
pytest.param((np.array([3, 0]), np.array([2, 0])), id="2d-arr-arr-unsorted-two-shards"),
1281+
pytest.param((np.array([3, 1, 2]), 1, np.array([0, 2])), id="3d-arr-int-arr"),
1282+
pytest.param((1, np.array([0, 2]), np.array([1, 3])), id="3d-int-arr-arr"),
1283+
pytest.param((np.array([3, 1]), np.array([0, 2]), 2), id="3d-arr-arr-int"),
1284+
pytest.param(
1285+
(np.array([3, 1, 2]), np.array([0, 2]), np.array([1, 3])), id="3d-arr-arr-arr"
1286+
),
1287+
pytest.param((np.array([3]), np.array([0, 2]), 1), id="3d-arr1-arr-int"),
1288+
],
1289+
)
1290+
def test_sharding_orthogonal_set_multiple_array_dims(
1291+
selection: tuple[int | npt.NDArray[np.intp], ...], nested: bool, pipeline_path: str
1292+
) -> None:
12761293
"""Orthogonal set with more than one array-indexed dimension.
12771294
1278-
``OrthogonalIndexer`` converts such a chunk selection to an ``np.ix_`` pair
1295+
``OrthogonalIndexer`` converts such a chunk selection to an ``np.ix_`` tuple
12791296
of broadcastable arrays before handing it to the codec pipeline. The
12801297
sharding codec re-derives an indexer from that selection and gets a
12811298
``CoordinateIndexer``, whose projections address the value buffer flat.
12821299
Regression test for the resulting shape mismatch on write.
12831300
1301+
An integer index alongside the arrays is the case that a shape-equality
1302+
guard misses: ``OrthogonalIndexer`` drops that axis from the value but
1303+
``np.ix_`` keeps it as a length-1 axis in the chunk selection, so the value
1304+
and the re-derived indexer's ``sel_shape`` differ in rank while agreeing in
1305+
element count.
1306+
12841307
Parametrized over both pipelines because the partial-encode path is
12851308
written twice -- ``_encode_partial_single`` for ``BatchedCodecPipeline``
1286-
and ``_encode_partial_sync`` for ``FusedCodecPipeline`` -- and each
1287-
derives its own indexer.
1309+
and ``_encode_partial_sync`` for ``FusedCodecPipeline``.
12881310
"""
1289-
inner = ShardingCodec(chunk_shape=(1, 1), codecs=(BytesCodec(),))
1290-
serializer = ShardingCodec(chunk_shape=(2, 2), codecs=((inner,) if nested else (BytesCodec(),)))
1291-
base = np.arange(16, dtype="int32").reshape(4, 4)
1292-
selection = (np.array([3, 1, 2]), np.array([0, 2]))
1293-
value = np.arange(6, dtype="int32").reshape(3, 2) + 100
1311+
ndim = len(selection)
1312+
shape = (4,) * ndim
1313+
inner = ShardingCodec(chunk_shape=(1,) * ndim, codecs=(BytesCodec(),))
1314+
serializer = ShardingCodec(
1315+
chunk_shape=(2,) * ndim, codecs=((inner,) if nested else (BytesCodec(),))
1316+
)
1317+
base = np.arange(4**ndim, dtype="int32").reshape(shape)
1318+
ix = np.ix_(*(np.atleast_1d(s) for s in selection))
1319+
# The value is shaped like the orthogonal result: integer axes dropped.
1320+
value_shape = tuple(len(s) for s in selection if not isinstance(s, int))
1321+
value = np.arange(math.prod(value_shape), dtype="int32").reshape(value_shape) + 100
12941322

12951323
with zarr.config.set({"codec_pipeline.path": pipeline_path}):
12961324
a = zarr.create_array(
12971325
MemoryStore(),
1298-
shape=base.shape,
1299-
chunks=(2, 4),
1326+
shape=shape,
1327+
chunks=(2,) + (4,) * (ndim - 1),
13001328
dtype=base.dtype,
13011329
serializer=serializer,
13021330
compressors=None,
@@ -1306,6 +1334,6 @@ def test_sharding_orthogonal_set_multiple_array_dims(nested: bool, pipeline_path
13061334
a.oindex[selection] = value
13071335

13081336
expected = base.copy()
1309-
expected[np.ix_(*selection)] = value
1337+
expected[ix] = value.reshape(expected[ix].shape)
13101338
assert np.array_equal(a[:], expected)
13111339
assert np.array_equal(a.oindex[selection], value)

tests/test_indexing.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ def zarr_array_from_numpy_array(
5151
store: StorePath,
5252
a: npt.NDArray[Any],
5353
chunk_shape: tuple[int, ...] | None = None,
54+
shards: tuple[int, ...] | None = None,
5455
) -> zarr.Array:
5556
z = zarr.create_array(
5657
store=store / str(uuid4()),
5758
shape=a.shape,
5859
dtype=a.dtype,
5960
chunks=chunk_shape or a.shape,
61+
shards=shards,
6062
chunk_key_encoding={"name": "v2", "separator": "."},
6163
)
6264
z[()] = a
@@ -2103,23 +2105,51 @@ def test_zero_sized_chunks(store: StorePath, shape: list[int]) -> None:
21032105
assert_array_equal(z[...], np.zeros(shape, dtype="f8"))
21042106

21052107

2106-
@pytest.mark.parametrize("store", ["memory"], indirect=["store"])
2107-
def test_vectorized_indexing_incompatible_shape(store) -> None:
2108-
"""Regression for GH2469: vectorized set-indexing raises ValueError when the value shape is incompatible with the indexer shape."""
2109-
# GH2469
2110-
shape = (4, 4)
2111-
chunks = (2, 2)
2112-
fill_value = 32767
2113-
arr = zarr.create(
2114-
shape,
2115-
store=store,
2116-
chunks=chunks,
2117-
dtype=np.int16,
2118-
fill_value=fill_value,
2119-
codecs=[zarr.codecs.BytesCodec(), zarr.codecs.BloscCodec()],
2120-
)
2121-
with pytest.raises(ValueError, match="Attempting to set"):
2122-
arr[np.array([1, 2]), np.array([1, 2])] = np.array([[-1, -2], [-3, -4]])
2108+
@pytest.mark.parametrize(
2109+
"pipeline_path",
2110+
[
2111+
"zarr.core.codec_pipeline.BatchedCodecPipeline",
2112+
"zarr.core.codec_pipeline.FusedCodecPipeline",
2113+
],
2114+
ids=["batched", "fused"],
2115+
)
2116+
@pytest.mark.parametrize("shards", [None, (4, 4)], ids=["chunked", "sharded"])
2117+
@pytest.mark.parametrize(
2118+
("kind", "selection", "value_shape"),
2119+
[
2120+
# GH2469: array-level check, the value has twice the selected elements.
2121+
pytest.param("vindex", (np.array([1, 2]), np.array([1, 2])), (2, 2), id="coord-2d-value"),
2122+
# Right element count, wrong rank: a mask takes a flat value.
2123+
pytest.param("vindex", np.eye(4, dtype=bool), (2, 2), id="mask-2d-value"),
2124+
# Right element count, an axis the selection does not have.
2125+
pytest.param(
2126+
"oindex", (np.array([3, 1, 2]), np.array([0, 2])), (3, 2, 1), id="oindex-extra-axis"
2127+
),
2128+
],
2129+
)
2130+
def test_set_selection_rejects_value_with_wrong_rank(
2131+
store: StorePath,
2132+
kind: str,
2133+
selection: Any,
2134+
value_shape: tuple[int, ...],
2135+
shards: tuple[int, ...] | None,
2136+
pipeline_path: str,
2137+
) -> None:
2138+
"""A value whose rank does not fit the selection raises regardless of storage layout.
2139+
2140+
The sharding codec re-derives an indexer from the selection it is handed
2141+
and ravels the value when it is the selection's broadcast shape minus
2142+
integer-indexed axes. Any other rank must fail on a sharded array exactly
2143+
as it does on a chunked one; an element count that happens to match is
2144+
not grounds to accept it. Only the rejection is asserted: a write that
2145+
fails inside the chunk merge may already have touched other chunks.
2146+
"""
2147+
a = np.zeros((4, 4), dtype=np.int32)
2148+
value = np.arange(np.prod(value_shape), dtype=np.int32).reshape(value_shape)
2149+
with zarr.config.set({"codec_pipeline.path": pipeline_path}):
2150+
z = zarr_array_from_numpy_array(store, a, chunk_shape=(2, 2), shards=shards)
2151+
with pytest.raises(ValueError, match="Attempting to set|shape mismatch"):
2152+
getattr(z, kind)[selection] = value
21232153

21242154

21252155
def test_iter_chunk_regions():

0 commit comments

Comments
 (0)