Repository: HypothesisWorks/hypothesis

Issue:
Generate negative indices from the `slices()` strategy
For example, `slices(size=3)` should be able to generate `slice(-3, -1)` as an equivalent form of `slice(0, 2)`.  We should also sometimes draw `step=None` if `step=1` would be allowed.  It would also be nice to support slicing length-zero sequences, i.e. `if size == 0:  return builds(slice, none(), none(), none() | integers().filter(bool))`.

Both would be helpful for downstream users - see e.g. https://github.com/Quansight/ndindex/issues/46.


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/validation.py b/hypothesis-python/src/hypothesis/internal/validation.py
--- a/hypothesis-python/src/hypothesis/internal/validation.py
+++ b/hypothesis-python/src/hypothesis/internal/validation.py
@@ -110,7 +110,7 @@ def check_valid_size(value, name):
 
     Otherwise raises InvalidArgument.
     """
-    if value is None and name != "min_size":
+    if value is None and name not in ("min_size", "size"):
         return
     check_type(int, value, name)
     if value < 0:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -2173,15 +2173,16 @@ def functions(
 def slices(draw: Any, size: int) -> slice:
     """Generates slices that will select indices up to the supplied size
 
-    Generated slices will have start and stop indices that range from 0 to size - 1
+    Generated slices will have start and stop indices that range from -size to size - 1
     and will step in the appropriate direction. Slices should only produce an empty selection
     if the start and end are the same.
 
     Examples from this strategy shrink toward 0 and smaller values
     """
-    check_valid_integer(size, "size")
-    if size is None or size < 1:
-        raise InvalidArgument("size=%r must be at least one" % size)
+    check_valid_size(size, "size")
+    if size == 0:
+        step = draw(none() | integers().filter(bool))
+        return slice(None, None, step)
 
     min_start = min_stop = 0
     max_start = max_stop = size
@@ -2205,4 +2206,9 @@ def slices(draw: Any, size: int) -> slice:
     if (stop or 0) < (start or 0):
         step *= -1
 
+    if draw(booleans()) and start is not None:
+        start -= size
+    if draw(booleans()) and stop is not None:
+        stop -= size
+
     return slice(start, stop, step)


Test patch:
diff --git a/hypothesis-python/tests/cover/test_direct_strategies.py b/hypothesis-python/tests/cover/test_direct_strategies.py
--- a/hypothesis-python/tests/cover/test_direct_strategies.py
+++ b/hypothesis-python/tests/cover/test_direct_strategies.py
@@ -170,7 +170,6 @@ def fn_ktest(*fnkwargs):
     (ds.characters, {"max_codepoint": "1"}),
     (ds.characters, {"whitelist_categories": []}),
     (ds.characters, {"whitelist_categories": ["Nd"], "blacklist_categories": ["Nd"]}),
-    (ds.slices, {"size": 0}),
     (ds.slices, {"size": None}),
     (ds.slices, {"size": "chips"}),
     (ds.slices, {"size": -1}),
diff --git a/hypothesis-python/tests/cover/test_slices.py b/hypothesis-python/tests/cover/test_slices.py
--- a/hypothesis-python/tests/cover/test_slices.py
+++ b/hypothesis-python/tests/cover/test_slices.py
@@ -24,14 +24,16 @@
 @use_several_sizes
 def test_stop_stays_within_bounds(size):
     assert_all_examples(
-        st.slices(size), lambda x: x.stop is None or (x.stop >= 0 and x.stop <= size)
+        st.slices(size),
+        lambda x: x.stop is None or (x.stop >= -size and x.stop <= size),
     )
 
 
 @use_several_sizes
 def test_start_stay_within_bounds(size):
     assert_all_examples(
-        st.slices(size), lambda x: x.start is None or (x.start >= 0 and x.start <= size)
+        st.slices(size),
+        lambda x: x.start is None or (x.start >= -size and x.start <= size),
     )
 
 
@@ -45,9 +47,9 @@ def test_step_stays_within_bounds(size):
         st.slices(size),
         lambda x: (
             x.indices(size)[0] + x.indices(size)[2] <= size
-            and x.indices(size)[0] + x.indices(size)[2] >= -1
+            and x.indices(size)[0] + x.indices(size)[2] >= -size
         )
-        or x.start == x.stop,
+        or x.start % size == x.stop % size,
     )
 
 
@@ -98,3 +100,9 @@ def test_start_will_equal_0(size):
 @settings(deadline=None)
 def test_start_will_equal_stop(size):
     find_any(st.slices(size), lambda x: x.start == x.stop)
+
+
+def test_size_is_equal_0():
+    assert_all_examples(
+        st.slices(0), lambda x: x.step != 0 and x.start is None and x.stop is None
+    )