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/3706.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow NumPy ints as input when declaring a shape.
16 changes: 11 additions & 5 deletions src/zarr/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
overload,
)

import numpy as np
from typing_extensions import ReadOnly

from zarr.core.config import config as zarr_config
Expand All @@ -37,7 +38,7 @@
ZMETADATA_V2_JSON = ".zmetadata"

BytesLike = bytes | bytearray | memoryview
ShapeLike = Iterable[int] | int
ShapeLike = Iterable[int | np.integer[Any]] | int | np.integer[Any]
# For backwards compatibility
ChunkCoords = tuple[int, ...]
ZarrFormat = Literal[2, 3]
Expand Down Expand Up @@ -185,23 +186,28 @@ def parse_named_configuration(


def parse_shapelike(data: ShapeLike) -> tuple[int, ...]:
if isinstance(data, int):
"""
Parse a shape-like input into an explicit shape.
"""
if isinstance(data, int | np.integer):
if data < 0:
raise ValueError(f"Expected a non-negative integer. Got {data} instead")
return (data,)
return (int(data),)
try:
data_tuple = tuple(data)
except TypeError as e:
msg = f"Expected an integer or an iterable of integers. Got {data} instead."
raise TypeError(msg) from e
Comment on lines 196 to 200
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this be consolidated with L210?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's invalid stuff (dict keys, strings) that could come out of an iterable data and be converted to int without any error. If data is ["0", "1"], then tuple(int(x) for x in data) will not error, and then we are accepting strings as shapes, which I didn't want to do here. So I think we need to end up calling tuple twice.


if not all(isinstance(v, int) for v in data_tuple):
if not all(isinstance(v, int | np.integer) for v in data_tuple):
msg = f"Expected an iterable of integers. Got {data} instead."
raise TypeError(msg)
if not all(v > -1 for v in data_tuple):
msg = f"Expected all values to be non-negative. Got {data} instead."
raise ValueError(msg)
return data_tuple

# cast NumPy scalars to plain python ints
return tuple(int(x) for x in data_tuple)


def parse_fill_value(data: Any) -> Any:
Expand Down
14 changes: 10 additions & 4 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Iterable
from typing import TYPE_CHECKING, get_args

import numpy as np
Expand All @@ -15,7 +16,6 @@
from zarr.core.config import parse_indexing_order

if TYPE_CHECKING:
from collections.abc import Iterable
from typing import Any, Literal


Expand Down Expand Up @@ -115,9 +115,15 @@ def test_parse_shapelike_invalid_iterable_values(data: Any) -> None:
parse_shapelike(data)


@pytest.mark.parametrize("data", [range(10), [0, 1, 2, 3], (3, 4, 5), ()])
def test_parse_shapelike_valid(data: Iterable[int]) -> None:
assert parse_shapelike(data) == tuple(data)
@pytest.mark.parametrize(
"data", [range(10), [0, 1, 2, np.uint64(3)], (3, 4, 5), (), 1, np.uint8(1)]
)
def test_parse_shapelike_valid(data: Iterable[int] | int) -> None:
if isinstance(data, Iterable):
expected = tuple(data)
else:
expected = (data,)
assert parse_shapelike(data) == expected


# todo: more dtypes
Expand Down