Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/4257.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Numpy integers are accepted as chunk sizes again. Since 3.3.0 a per-dimension chunk size that was a numpy integer (e.g. `chunks=(np.int64(2), np.int64(2))`, as produced by any computed chunk shape) raised `TypeError: 'numpy.int64' object is not iterable`, because the scalar chunk path narrowed on `int` while its caller dispatched on `numbers.Integral`. Numpy arrays are now also accepted as chunk specifications, and a chunk specification that is neither an integer nor iterable now reports the offending value instead of failing with an opaque iteration error.
27 changes: 18 additions & 9 deletions src/zarr/core/chunk_grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,17 +729,26 @@ def normalize_chunks_1d(
overhang the span. The actual data extent of each chunk is determined
by the chunk grid at runtime, not by this function.
"""
if chunks == -1:
return np.array([span], dtype=np.int64)
if isinstance(chunks, int):
if chunks <= 0:
raise ValueError(f"Chunk size must be positive, got {chunks}")
# `numbers.Integral` rather than `int` so that numpy integer scalars (which are not
# `int` subclasses) take the uniform-chunk path instead of being treated as a sequence.
if isinstance(chunks, numbers.Integral):
chunk_size = int(chunks)
if chunk_size == -1:
return np.array([span], dtype=np.int64)
if chunk_size <= 0:
raise ValueError(f"Chunk size must be positive, got {chunk_size}")
if span == 0:
return np.array([chunks], dtype=np.int64)
n = ceildiv(span, chunks)
return np.full(n, chunks, dtype=np.int64)
return np.array([chunk_size], dtype=np.int64)
n = ceildiv(span, chunk_size)
return np.full(n, chunk_size, dtype=np.int64)
else:
chunk_list = list(chunks)
try:
chunk_list = list(chunks) # type: ignore[arg-type]
except TypeError:
raise TypeError(
f"Chunk specification must be an integer or an iterable of integers; got "
f"{chunks!r} of type {type(chunks).__name__}."
) from None
if not chunk_list:
raise ValueError("Chunk specification must not be empty")
non_int = [
Expand Down
2 changes: 1 addition & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def test_create(memory_store: Store) -> None:
z = create(shape=(400.5, 100), store=store, overwrite=True) # type: ignore[arg-type]

# create array with float chunk shape
with pytest.raises(TypeError, match="'float' object is not iterable"):
with pytest.raises(TypeError, match="Chunk specification must be an integer or an iterable"):
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type]


Expand Down
24 changes: 24 additions & 0 deletions tests/test_chunk_grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ def test_guess_chunks(shape: tuple[int, ...], itemsize: int) -> None:
(10, (0,), ((10,),)),
((5, 10), (0, 100), ((5,), (10,) * 10)),
((5, 10), (20, 0), ((5, 5, 5, 5), (10,))),
# numpy integers are accepted anywhere a python int is, whether as the scalar
# convenience form, as per-dimension entries, or as the `-1` sentinel.
(np.int64(10), (100,), ((10,) * 10,)),
((np.int64(2), np.int64(2)), (4, 4), ((2, 2), (2, 2))),
((1, 3, np.int64(16), np.int64(16)), (1, 3, 32, 32), ((1,), (3,), (16, 16), (16, 16))),
((np.int32(30), np.int64(-1)), (100, 20), ((30, 30, 30, 30), (20,))),
(np.array([10, 10]), (100, 100), ((10,) * 10, (10,) * 10)),
# rectilinear chunks given as numpy arrays
((np.array([60, 40]), np.array([50, 50])), (100, 100), ((60, 40), (50, 50))),
],
)
def test_normalize_chunks(
Expand Down Expand Up @@ -142,7 +151,22 @@ def test_chunk_layout_nested() -> None:
id="negative-uniform",
msg="Chunk size must be positive",
),
ExpectFail(
input=(np.int64(0), 100),
exception=ValueError,
id="zero-uniform-numpy",
msg="Chunk size must be positive",
),
ExpectFail(input=([], 100), exception=ValueError, id="empty-list", msg="must not be empty"),
# Scalars that are neither integers nor iterable name themselves in the error,
# rather than surfacing an opaque "object is not iterable" from `list(chunks)`.
ExpectFail(
input=(2.5, 100),
exception=TypeError,
id="non-iterable-scalar",
msg="must be an integer or an iterable of integers; got 2.5 of type float",
escape=True,
),
ExpectFail(
input=([10, -1, 10], 100),
exception=ValueError,
Expand Down
Loading