Skip to content
Open
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/4518.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`arr[[]]`, and the same empty-list selection inside a tuple, an `oindex` selection or a `vindex` selection, used to raise `IndexError: unsupported selection item for basic indexing`, even though `IntSequence`, the declared selection type, includes `list[int]` and the same selection written as an empty integer array already worked. An empty list is now treated as an empty integer selection, matching NumPy, so `arr[[]]` returns an empty array instead of raising.
9 changes: 6 additions & 3 deletions src/zarr/core/indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ def is_bool(x: Any) -> TypeGuard[bool | np.bool_]:


def is_integer_list(x: Any) -> TypeGuard[list[int]]:
"""True if x is a list of integers."""
return isinstance(x, list) and len(x) > 0 and all(is_integer(i) for i in x)
"""True if x is a list of integers (including an empty list, as in NumPy)."""
return isinstance(x, list) and all(is_integer(i) for i in x)


def is_bool_list(x: Any) -> TypeGuard[list[bool | np.bool_]]:
Expand Down Expand Up @@ -510,8 +510,11 @@ def replace_ellipsis(selection: Any, shape: tuple[int, ...]) -> SelectionNormali


def replace_lists(selection: SelectionNormalized) -> SelectionNormalized:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider this instead:

def replace_lists(selection: SelectionNormalized) -> SelectionNormalized:
    # An empty list has no dtype to infer; NumPy indexes with it as an empty integer array.
    return tuple(
        cast("ArrayOfIntOrBool", np.asarray(dim_sel, dtype=np.intp if len(dim_sel) == 0 else None))
        if isinstance(dim_sel, list)
        else dim_sel
        for dim_sel in selection
    )

# An empty list has no dtype to infer; NumPy indexes with it as an empty integer array.
return tuple(
cast("ArrayOfIntOrBool", np.asarray(dim_sel)) if isinstance(dim_sel, list) else dim_sel
cast("ArrayOfIntOrBool", np.asarray(dim_sel, dtype=np.intp if len(dim_sel) == 0 else None))
if isinstance(dim_sel, list)
else dim_sel
for dim_sel in selection
)

Expand Down
41 changes: 41 additions & 0 deletions tests/test_indexing.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,47 @@ def test_get_basic_selection_2d_rejects_list_in_tuple(store: StorePath) -> None:
z.get_basic_selection((slice(None), [0, 1]))


@pytest.mark.parametrize("zarr_format", [2, 3])
def test_empty_list_selection_matches_empty_array_and_numpy(zarr_format: ZarrFormat) -> None:
"""An empty list is an empty integer selection, as it is in NumPy.

IntSequence, the declared selection type, includes list[int], so an empty list
has to behave like the equivalent empty integer array instead of being rejected
by basic indexing.
"""
expected = np.arange(16, dtype="i4").reshape(4, 4)
arr = zarr.create_array({}, data=expected, chunks=(2, 2), zarr_format=zarr_format)
empty = np.array([], dtype=np.intp)

# Short form, orthogonal form and vectorized form all agree with NumPy.
assert_array_equal(arr[[]], expected[[]])
assert_array_equal(arr[[], :], expected[[], :])
assert_array_equal(arr[:, []], expected[:, []])
assert_array_equal(arr[[], []], expected[[], []])
assert_array_equal(arr[[], 0], expected[[], 0])
assert_array_equal(arr[0, []], expected[0, []])
assert_array_equal(arr.oindex[[]], expected[[]])
assert_array_equal(arr.oindex[[], :], expected[[], :])
# The equivalent empty array selection was already accepted.
assert_array_equal(arr[empty], expected[empty])
assert_array_equal(arr[empty, :], expected[empty, :])

# A bare list on a 1D array routes through vectorized indexing.
expected_1d = np.arange(4, dtype="i4")
arr_1d = zarr.create_array({}, data=expected_1d, chunks=(2,), zarr_format=zarr_format)
assert_array_equal(arr_1d[[]], expected_1d[[]])
assert_array_equal(arr_1d.vindex[[]], expected_1d[[]])

# Non-empty integer lists and boolean lists are unaffected.
assert_array_equal(arr[[0]], expected[[0]])
assert_array_equal(arr[[True, False, True, False]], expected[[True, False, True, False]])

# Writing through an empty selection is a no-op.
arr[[], :] = 1
arr[:, []] = 1
assert_array_equal(arr[:], expected)


def test_fancy_indexing_fallback_on_get_setitem(store: StorePath) -> None:
"""Paired integer-list indexing falls back to vectorized (fancy) get and set via `__getitem__`/`__setitem__`."""
z = zarr_array_from_numpy_array(store, np.zeros((20, 20)))
Expand Down
Loading