From a8f93759a34f7b15107878d2de71c6d549d44bc0 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 17 Feb 2025 15:08:18 +0100 Subject: [PATCH 1/4] TST: fix tests for disallowed array indexing Several assertions were raising for "wrong" reasons. --- array_api_strict/tests/test_array_object.py | 49 +++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index edfa073..87fdbc3 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -45,35 +45,37 @@ def test_validate_index(): a = ones((3, 4)) # Out of bounds slices are not allowed - assert_raises(IndexError, lambda: a[:4]) - assert_raises(IndexError, lambda: a[:-4]) - assert_raises(IndexError, lambda: a[:3:-1]) - assert_raises(IndexError, lambda: a[:-5:-1]) - assert_raises(IndexError, lambda: a[4:]) - assert_raises(IndexError, lambda: a[-4:]) - assert_raises(IndexError, lambda: a[4::-1]) - assert_raises(IndexError, lambda: a[-4::-1]) - - assert_raises(IndexError, lambda: a[...,:5]) - assert_raises(IndexError, lambda: a[...,:-5]) - assert_raises(IndexError, lambda: a[...,:5:-1]) - assert_raises(IndexError, lambda: a[...,:-6:-1]) - assert_raises(IndexError, lambda: a[...,5:]) - assert_raises(IndexError, lambda: a[...,-5:]) - assert_raises(IndexError, lambda: a[...,5::-1]) - assert_raises(IndexError, lambda: a[...,-5::-1]) + assert_raises(IndexError, lambda: a[:4, 0]) + assert_raises(IndexError, lambda: a[:-4, 0]) + assert_raises(IndexError, lambda: a[:3:-1]) # XXX raises for a wrong reason + assert_raises(IndexError, lambda: a[:-5:-1, 0]) + assert_raises(IndexError, lambda: a[4:, 0]) + assert_raises(IndexError, lambda: a[-4:, 0]) + assert_raises(IndexError, lambda: a[4::-1, 0]) + assert_raises(IndexError, lambda: a[-4::-1, 0]) + + assert_raises(IndexError, lambda: a[..., :5]) + assert_raises(IndexError, lambda: a[..., :-5]) + assert_raises(IndexError, lambda: a[..., :5:-1]) + assert_raises(IndexError, lambda: a[..., :-6:-1]) + assert_raises(IndexError, lambda: a[..., 5:]) + assert_raises(IndexError, lambda: a[..., -5:]) + assert_raises(IndexError, lambda: a[..., 5::-1]) + assert_raises(IndexError, lambda: a[..., -5::-1]) # Boolean indices cannot be part of a larger tuple index - assert_raises(IndexError, lambda: a[a[:,0]==1,0]) - assert_raises(IndexError, lambda: a[a[:,0]==1,...]) - assert_raises(IndexError, lambda: a[..., a[0]==1]) + assert_raises(IndexError, lambda: a[a[:, 0] == 1, 0]) + assert_raises(IndexError, lambda: a[a[:, 0] == 1, ...]) + assert_raises(IndexError, lambda: a[..., a[0] == 1]) assert_raises(IndexError, lambda: a[[True, True, True]]) assert_raises(IndexError, lambda: a[(True, True, True),]) # Integer array indices are not allowed (except for 0-D) - idx = asarray([[0, 1]]) - assert_raises(IndexError, lambda: a[idx]) - assert_raises(IndexError, lambda: a[idx,]) + idx = asarray([0, 1]) + assert_raises(IndexError, lambda: a[idx, 0]) + assert_raises(IndexError, lambda: a[0, idx]) + + # Array-likes (lists, tuples) are not allowed as indices assert_raises(IndexError, lambda: a[[0, 1]]) assert_raises(IndexError, lambda: a[(0, 1), (0, 1)]) assert_raises(IndexError, lambda: a[[0, 1]]) @@ -87,6 +89,7 @@ def test_validate_index(): assert_raises(IndexError, lambda: a[0,]) assert_raises(IndexError, lambda: a[0]) assert_raises(IndexError, lambda: a[:]) + assert_raises(IndexError, lambda: a[idx]) def test_promoted_scalar_inherits_device(): device1 = Device("device1") From c514dbc8f4b96de357fd0f387a7259b590a1e4a4 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 17 Feb 2025 16:26:50 +0100 Subject: [PATCH 2/4] ENH: allow 1D integer array indices --- array_api_strict/_array_object.py | 20 ++++++-- array_api_strict/tests/test_array_object.py | 51 +++++++++++++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index afee030..6d63577 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -395,6 +395,8 @@ def _validate_index(self, key): single_axes = [] n_ellipsis = 0 key_has_mask = False + key_has_index_array = False + key_has_slices = False for i in _key: if i is not None: nonexpanding_key.append(i) @@ -403,6 +405,8 @@ def _validate_index(self, key): if isinstance(i, Array): if i.dtype in _boolean_dtypes: key_has_mask = True + elif i.dtype in _integer_dtypes: + key_has_index_array = True single_axes.append(i) else: # i must not be an array here, to avoid elementwise equals @@ -410,6 +414,8 @@ def _validate_index(self, key): n_ellipsis += 1 else: single_axes.append(i) + if isinstance(i, slice): + key_has_slices = True n_single_axes = len(single_axes) if n_ellipsis > 1: @@ -427,6 +433,12 @@ def _validate_index(self, key): "specified in the Array API." ) + if (key_has_index_array and (n_ellipsis > 0 or key_has_slices or key_has_mask)): + raise IndexError( + "Integer index arrays are only allowed with integer indices; " + f"got {key}." + ) + if n_ellipsis == 0: indexed_shape = self.shape else: @@ -485,11 +497,11 @@ def _validate_index(self, key): if not get_array_api_strict_flags()['boolean_indexing']: raise RuntimeError("The boolean_indexing flag has been disabled for array-api-strict") - elif i.dtype in _integer_dtypes and i.ndim != 0: + elif i.dtype in _integer_dtypes and i.ndim > 1: raise IndexError( - f"Single-axes index {i} is a non-zero-dimensional " - "integer array, but advanced integer indexing is not " - "specified in the Array API." + f"Single-axes index {i} is a multi-dimensional " + "integer array, but advanced integer indexing is only " + "specified in the Array API for 1D index arrays." ) elif isinstance(i, tuple): raise IndexError( diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index 87fdbc3..5747cb1 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from .. import ones, asarray, result_type, all, equal +from .. import ones, arange, reshape, asarray, result_type, all, equal from .._array_object import Array, CPU_DEVICE, Device from .._dtypes import ( _all_dtypes, @@ -70,11 +70,25 @@ def test_validate_index(): assert_raises(IndexError, lambda: a[[True, True, True]]) assert_raises(IndexError, lambda: a[(True, True, True),]) - # Integer array indices are not allowed (except for 0-D) - idx = asarray([0, 1]) + # Integer array indices are not allowed (except for 0-D or 1D) + idx = asarray([[0, 1]]) # idx.ndim == 2 assert_raises(IndexError, lambda: a[idx, 0]) assert_raises(IndexError, lambda: a[0, idx]) + # Mixing 1D integer array indices with slices, ellipsis or booleans is not allowed + idx = asarray([0, 1]) + assert_raises(IndexError, lambda: a[..., idx]) + assert_raises(IndexError, lambda: a[:, idx]) + assert_raises(IndexError, lambda: a[asarray([True, True]), idx]) + + # 1D integer array indices must have the same length + idx1 = asarray([0, 1]) + idx2 = asarray([0, 1, 1]) + assert_raises(IndexError, lambda: a[idx1, idx2]) + + # Non-integer array indices are not allowed + assert_raises(IndexError, lambda: a[ones(2), 0]) + # Array-likes (lists, tuples) are not allowed as indices assert_raises(IndexError, lambda: a[[0, 1]]) assert_raises(IndexError, lambda: a[(0, 1), (0, 1)]) @@ -91,6 +105,37 @@ def test_validate_index(): assert_raises(IndexError, lambda: a[:]) assert_raises(IndexError, lambda: a[idx]) + +def test_indexing_arrays(): + # indexing with 1D integer arrays and mixes of integers and 1D integer are allowed + + # 1D array + a = arange(5) + idx = asarray([1, 0, 1, 2, -1]) + a_idx = a[idx] + + a_idx_loop = asarray([a[idx[i]] for i in range(idx.shape[0])]) + assert all(a_idx == a_idx_loop) + + # setitem with arrays is not allowed # XXX + # with assert_raises(IndexError): + # a[idx] = 42 + + # mixed array and integer indexing + a = reshape(arange(3*4), (3, 4)) + idx = asarray([1, 0, 1, 2, -1]) + a_idx = a[idx, 1] + + a_idx_loop = asarray([a[idx[i], 1] for i in range(idx.shape[0])]) + assert all(a_idx == a_idx_loop) + + + # index with two arrays + a_idx = a[idx, idx] + a_idx_loop = asarray([a[idx[i], idx[i]] for i in range(idx.shape[0])]) + assert all(a_idx == a_idx_loop) + + def test_promoted_scalar_inherits_device(): device1 = Device("device1") x = asarray([1., 2, 3], device=device1) From 36a370ada23e25206d3398a60d77c0d5a08e0636 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 18 Feb 2025 11:17:45 +0100 Subject: [PATCH 3/4] ENH: fancy indexing __setitem__ is not allowed --- array_api_strict/_array_object.py | 7 +++++-- array_api_strict/tests/test_array_object.py | 11 +++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 6d63577..1a8c566 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -327,7 +327,7 @@ def _normalize_two_args(x1, x2) -> Tuple[Array, Array]: # Note: A large fraction of allowed indices are disallowed here (see the # docstring below) - def _validate_index(self, key): + def _validate_index(self, key, op="getitem"): """ Validate an index according to the array API. @@ -390,6 +390,9 @@ def _validate_index(self, key): "zero-dimensional integer arrays and boolean arrays " "are specified in the Array API." ) + if op == "setitem": + if isinstance(i, Array) and i.dtype in _integer_dtypes: + raise IndexError("Fancy indexing __setitem__ is not supported.") nonexpanding_key = [] single_axes = [] @@ -914,7 +917,7 @@ def __setitem__( """ # Note: Only indices required by the spec are allowed. See the # docstring of _validate_index - self._validate_index(key) + self._validate_index(key, op="setitem") if isinstance(key, Array): # Indexing self._array with array_api_strict arrays can be erroneous key = key._array diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index 5747cb1..6a381d4 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -117,9 +117,9 @@ def test_indexing_arrays(): a_idx_loop = asarray([a[idx[i]] for i in range(idx.shape[0])]) assert all(a_idx == a_idx_loop) - # setitem with arrays is not allowed # XXX - # with assert_raises(IndexError): - # a[idx] = 42 + # setitem with arrays is not allowed + with assert_raises(IndexError): + a[idx] = 42 # mixed array and integer indexing a = reshape(arange(3*4), (3, 4)) @@ -129,12 +129,15 @@ def test_indexing_arrays(): a_idx_loop = asarray([a[idx[i], 1] for i in range(idx.shape[0])]) assert all(a_idx == a_idx_loop) - # index with two arrays a_idx = a[idx, idx] a_idx_loop = asarray([a[idx[i], idx[i]] for i in range(idx.shape[0])]) assert all(a_idx == a_idx_loop) + # setitem with arrays is not allowed + with assert_raises(IndexError): + a[idx, idx] = 42 + def test_promoted_scalar_inherits_device(): device1 = Device("device1") From 6664e6d241ca4fce8305821dd6a7ed143b5796c0 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 18 Feb 2025 20:04:30 +0100 Subject: [PATCH 4/4] ENH: allow ndim>1 indexing arrays --- array_api_strict/_array_object.py | 11 +++-------- array_api_strict/tests/test_array_object.py | 9 ++++----- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/array_api_strict/_array_object.py b/array_api_strict/_array_object.py index 1a8c566..0595594 100644 --- a/array_api_strict/_array_object.py +++ b/array_api_strict/_array_object.py @@ -498,14 +498,9 @@ def _validate_index(self, key, op="getitem"): "Array API when the array is the sole index." ) if not get_array_api_strict_flags()['boolean_indexing']: - raise RuntimeError("The boolean_indexing flag has been disabled for array-api-strict") - - elif i.dtype in _integer_dtypes and i.ndim > 1: - raise IndexError( - f"Single-axes index {i} is a multi-dimensional " - "integer array, but advanced integer indexing is only " - "specified in the Array API for 1D index arrays." - ) + raise RuntimeError( + "The boolean_indexing flag has been disabled for array-api-strict" + ) elif isinstance(i, tuple): raise IndexError( f"Single-axes index {i} is a tuple, but nested tuple " diff --git a/array_api_strict/tests/test_array_object.py b/array_api_strict/tests/test_array_object.py index 6a381d4..ef76c28 100644 --- a/array_api_strict/tests/test_array_object.py +++ b/array_api_strict/tests/test_array_object.py @@ -70,11 +70,6 @@ def test_validate_index(): assert_raises(IndexError, lambda: a[[True, True, True]]) assert_raises(IndexError, lambda: a[(True, True, True),]) - # Integer array indices are not allowed (except for 0-D or 1D) - idx = asarray([[0, 1]]) # idx.ndim == 2 - assert_raises(IndexError, lambda: a[idx, 0]) - assert_raises(IndexError, lambda: a[0, idx]) - # Mixing 1D integer array indices with slices, ellipsis or booleans is not allowed idx = asarray([0, 1]) assert_raises(IndexError, lambda: a[..., idx]) @@ -138,6 +133,10 @@ def test_indexing_arrays(): with assert_raises(IndexError): a[idx, idx] = 42 + # smoke test indexing with ndim > 1 arrays + idx = idx[..., None] + a[idx, idx] + def test_promoted_scalar_inherits_device(): device1 = Device("device1")