Repository: HypothesisWorks/hypothesis

Issue:
Stop generating flat indices in `xps.indices()`
The Array API was updated to remove flat indexing in https://github.com/data-apis/array-api/pull/272 i.e. providing a single-axis indexing expression to a higher-dimensional array. This means `xps.indices()` should be updated accordingly.

Already tried fixing this but got caught up with other things. Just writing an issue to keep track and jump-the-gun if others find this problem, but should get to soon.


Discussion:
(updated link to point to upstream repo, not our own issue with the same number)

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/_array_helpers.py b/hypothesis-python/src/hypothesis/extra/_array_helpers.py
--- a/hypothesis-python/src/hypothesis/extra/_array_helpers.py
+++ b/hypothesis-python/src/hypothesis/extra/_array_helpers.py
@@ -632,12 +632,24 @@ def _draw_loop_dimensions(self, data, use=None):
 
 
 class BasicIndexStrategy(st.SearchStrategy):
-    def __init__(self, shape, min_dims, max_dims, allow_ellipsis, allow_newaxis):
+    def __init__(
+        self,
+        shape,
+        min_dims,
+        max_dims,
+        allow_ellipsis,
+        allow_newaxis,
+        allow_fewer_indices_than_dims,
+    ):
         self.shape = shape
         self.min_dims = min_dims
         self.max_dims = max_dims
         self.allow_ellipsis = allow_ellipsis
         self.allow_newaxis = allow_newaxis
+        # allow_fewer_indices_than_dims=False will disable generating indices
+        # that don't cover all axes, i.e. indices that will flat index arrays.
+        # This is necessary for the Array API as such indices are not supported.
+        self.allow_fewer_indices_than_dims = allow_fewer_indices_than_dims
 
     def do_draw(self, data):
         # General plan: determine the actual selection up front with a straightforward
@@ -673,7 +685,7 @@ def do_draw(self, data):
             while j < len(result) and result[j] == slice(None):
                 j += 1
             result[i:j] = [Ellipsis]
-        else:
+        elif self.allow_fewer_indices_than_dims:  # pragma: no cover
             while result[-1:] == [slice(None, None)] and data.draw(st.integers(0, 7)):
                 result.pop()
         if len(result) == 1 and data.draw(st.booleans()):
diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py
--- a/hypothesis-python/src/hypothesis/extra/array_api.py
+++ b/hypothesis-python/src/hypothesis/extra/array_api.py
@@ -718,10 +718,6 @@ def indices(
     * ``allow_ellipsis`` specifies whether ``...`` is allowed in the index.
     """
     check_type(tuple, shape, "shape")
-    check_argument(
-        len(shape) != 0,
-        "No valid indices for zero-dimensional arrays",
-    )
     check_argument(
         all(isinstance(x, int) and x >= 0 for x in shape),
         f"shape={shape!r}, but all dimensions must be non-negative integers.",
@@ -754,6 +750,7 @@ def indices(
         max_dims=max_dims,
         allow_ellipsis=allow_ellipsis,
         allow_newaxis=False,
+        allow_fewer_indices_than_dims=False,
     )
 
 
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -880,6 +880,7 @@ def basic_indices(
         max_dims=max_dims,
         allow_ellipsis=allow_ellipsis,
         allow_newaxis=allow_newaxis,
+        allow_fewer_indices_than_dims=True,
     )
 
 


Test patch:
diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py
--- a/hypothesis-python/tests/array_api/test_argument_validation.py
+++ b/hypothesis-python/tests/array_api/test_argument_validation.py
@@ -207,7 +207,6 @@ def e(a, **kwargs):
         e(xps.indices, shape=(0, 0), max_dims=-1),
         e(xps.indices, shape=(0, 0), max_dims=1.0),
         e(xps.indices, shape=(0, 0), min_dims=2, max_dims=1),
-        e(xps.indices, shape=()),
         e(xps.indices, shape=5, min_dims=0),
         e(xps.indices, shape=(5,), min_dims=2),
         e(xps.indices, shape=(5,), max_dims=2),
diff --git a/hypothesis-python/tests/array_api/test_indices.py b/hypothesis-python/tests/array_api/test_indices.py
--- a/hypothesis-python/tests/array_api/test_indices.py
+++ b/hypothesis-python/tests/array_api/test_indices.py
@@ -32,17 +32,18 @@ def test_generate_indices_with_and_without_ellipsis():
     find_any(strat, lambda ix: Ellipsis not in ix)
 
 
-def test_generate_empty_tuple():
-    """Strategy generates empty tuples as indices."""
-    find_any(xps.indices(shape=(0, 0), allow_ellipsis=True), lambda ix: ix == ())
+@given(xps.indices(shape=(), allow_ellipsis=True))
+def test_generate_indices_for_0d_shape(idx):
+    """Strategy only generates empty tuples or Ellipsis as indices for an empty
+    shape."""
+    assert idx in [(), Ellipsis, (Ellipsis,)]
 
 
-def test_generate_non_tuples():
-    """Strategy generates non-tuples as indices."""
-    find_any(
-        xps.indices(shape=(0, 0), allow_ellipsis=True),
-        lambda ix: not isinstance(ix, tuple),
-    )
+def test_generate_tuples_and_non_tuples_for_1d_shape():
+    """Strategy can generate tuple and non-tuple indices with a 1-dimensional shape."""
+    strat = xps.indices(shape=(1,), allow_ellipsis=True)
+    find_any(strat, lambda ix: isinstance(ix, tuple))
+    find_any(strat, lambda ix: not isinstance(ix, tuple))
 
 
 def test_generate_long_ellipsis():
@@ -72,7 +73,7 @@ def test_indices_replaces_whole_axis_slices_with_ellipsis(idx):
 
 
 @given(xps.indices((3, 3, 3, 3, 3)))
-def test_indices_effeciently_generate_indexers(_):
+def test_efficiently_generate_indexers(_):
     """Generation is not too slow."""
 
 
@@ -82,7 +83,7 @@ def test_indices_effeciently_generate_indexers(_):
     allow_ellipsis=st.booleans(),
     data=st.data(),
 )
-def test_indices_generate_valid_indexers(shape, allow_ellipsis, data):
+def test_generate_valid_indices(shape, allow_ellipsis, data):
     """Strategy generates valid indices."""
     min_dims = data.draw(st.integers(0, len(shape)), label="min_dims")
     max_dims = data.draw(
@@ -98,25 +99,30 @@ def test_indices_generate_valid_indexers(shape, allow_ellipsis, data):
         label="indexer",
     )
 
+    _indexer = indexer if isinstance(indexer, tuple) else (indexer,)
     # Check that disallowed things are indeed absent
-    if isinstance(indexer, tuple):
-        assert 0 <= len(indexer) <= len(shape) + int(allow_ellipsis)
-    else:
-        assert 1 <= len(shape) + int(allow_ellipsis)
-    assert None not in shape
     if not allow_ellipsis:
-        assert Ellipsis not in shape
+        assert Ellipsis not in _indexer
+    assert None not in _indexer  # i.e. np.newaxis
+    # Check index is composed of valid objects
+    for i in _indexer:
+        assert isinstance(i, int) or isinstance(i, slice) or i == Ellipsis
+    # Check indexer does not flat index
+    if Ellipsis in _indexer:
+        assert sum(i == Ellipsis for i in _indexer) == 1
+        assert len(_indexer) <= len(shape) + 1  # Ellipsis can index 0 axes
+    else:
+        assert len(_indexer) == len(shape)
 
     if 0 in shape:
         # If there's a zero in the shape, the array will have no elements.
         array = xp.zeros(shape)
-        assert array.size == 0
+        assert array.size == 0  # sanity check
     elif math.prod(shape) <= 10 ** 5:
         # If it's small enough to instantiate, do so with distinct elements.
         array = xp.reshape(xp.arange(math.prod(shape)), shape)
     else:
         # We can't cheat on this one, so just try another.
         assume(False)
-
     # Finally, check that we can use our indexer without error
     array[indexer]
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -1128,6 +1128,18 @@ def test_basic_indices_replaces_whole_axis_slices_with_ellipsis(idx):
     assert slice(None) not in idx
 
 
+def test_basic_indices_can_generate_indices_not_covering_all_dims():
+    # These "flat indices" are skippable in the underlying BasicIndexStrategy,
+    # so we ensure we're definitely generating them for nps.basic_indices().
+    find_any(
+        nps.basic_indices(shape=(3, 3, 3)),
+        lambda ix: (
+            (not isinstance(ix, tuple) and ix != Ellipsis)
+            or (isinstance(ix, tuple) and Ellipsis not in ix and len(ix) < 3)
+        ),
+    )
+
+
 @given(
     shape=nps.array_shapes(min_dims=0, max_side=4)
     | nps.array_shapes(min_dims=0, min_side=0, max_side=10),