Repository: HypothesisWorks/hypothesis

Issue:
Add `st.slices(sequence_size)` strategy
Taking slices of an array index is really useful for Numpy, but not Numpy-specific - a `slices()` strategy could be useful for any standard library sequence type, to say nothing of other array libraries (not *all* of which depend on Numpy).

This issue proposes a simpler version of the [`slice_index`](https://github.com/rsokl/MyGrad/blob/62912d8bbb1c79ca3a72578a3e68720e4c29c594/tests/custom_strategies/__init__.py#L212) strategy from @rsokl's MyGrad library (see #1829), to be called `slices`.  At least for the first version, we will only support a single argument - an integer length of the sequence this slice could index.  The documentation could recommend using `builds(slice, ...)` for users who need to control specific values.

The implementation should shrink to selecting fewer elements and earlier elements, and smaller and positive step sizes.  It should only ever select zero elements if `start == stop`, i.e. not because the `step` value has the wrong sign.  I'd probably use `@composite` for the first draft.


Discussion:
Taking a shot at working on this

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/_strategies.py b/hypothesis-python/src/hypothesis/_strategies.py
--- a/hypothesis-python/src/hypothesis/_strategies.py
+++ b/hypothesis-python/src/hypothesis/_strategies.py
@@ -2217,3 +2217,42 @@ def functions(like=lambda: None, returns=none()):
             "but got non-callable like=%r" % (nicerepr(like),)
         )
     return FunctionStrategy(like, returns)
+
+
+@composite
+def slices(draw, size):
+    """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
+    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)
+    if size is None or size < 1:
+        raise InvalidArgument("size=%r must be at least one" % size)
+
+    min_start = min_stop = 0
+    max_start = max_stop = size
+    min_step = 1
+    # For slices start is inclusive and stop is exclusive
+    start = draw(integers(min_start, max_start) | none())
+    stop = draw(integers(min_stop, max_stop) | none())
+
+    # Limit step size to be reasonable
+    if start is None and stop is None:
+        max_step = size
+    elif start is None:
+        max_step = stop
+    elif stop is None:
+        max_step = start
+    else:
+        max_step = abs(start - stop)
+
+    step = draw(integers(min_step, max_step or 1))
+
+    if (stop or 0) < (start or 0):
+        step *= -1
+
+    return slice(start, stop, step)
diff --git a/hypothesis-python/src/hypothesis/strategies.py b/hypothesis-python/src/hypothesis/strategies.py
--- a/hypothesis-python/src/hypothesis/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies.py
@@ -55,6 +55,7 @@
     sampled_from,
     sets,
     shared,
+    slices,
     text,
     timedeltas,
     times,
@@ -105,6 +106,7 @@
     "sampled_from",
     "sets",
     "shared",
+    "slices",
     "text",
     "timedeltas",
     "times",


Test patch:
diff --git a/hypothesis-python/tests/cover/test_deadline.py b/hypothesis-python/tests/cover/test_deadline.py
--- a/hypothesis-python/tests/cover/test_deadline.py
+++ b/hypothesis-python/tests/cover/test_deadline.py
@@ -70,15 +70,15 @@ def test_flaky_slow(i):
 
 def test_deadlines_participate_in_shrinking():
     @settings(deadline=500)
-    @given(st.integers())
+    @given(st.integers(min_value=0))
     def slow_if_large(i):
-        if i >= 10000:
+        if i >= 1000:
             time.sleep(1)
 
     with capture_out() as o:
         with pytest.raises(DeadlineExceeded):
             slow_if_large()
-    assert "slow_if_large(i=10000)" in o.getvalue()
+    assert "slow_if_large(i=1000)" in o.getvalue()
 
 
 def test_keeps_you_well_above_the_deadline():
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
@@ -158,6 +158,11 @@ 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}),
+    (ds.slices, {"size": 2.3}),
 )
 def test_validates_keyword_arguments(fn, kwargs):
     with pytest.raises(InvalidArgument):
diff --git a/hypothesis-python/tests/cover/test_slices.py b/hypothesis-python/tests/cover/test_slices.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_slices.py
@@ -0,0 +1,104 @@
+# coding=utf-8
+#
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2019 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+from hypothesis import given, settings, strategies as st
+from tests.common.debug import assert_all_examples, find_any, minimal
+
+use_several_sizes = pytest.mark.parametrize("size", [1, 2, 5, 10, 100, 1000])
+
+
+@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)
+    )
+
+
+@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)
+    )
+
+
+@use_several_sizes
+def test_step_stays_within_bounds(size):
+    # indices -> (start, stop, step)
+    # Stop is exclusive so we use -1 as the floor.
+    # This uses the indices that slice produces to make this test more readable
+    # due to how splice processes None being a little complex
+    assert_all_examples(
+        st.slices(size),
+        lambda x: (
+            x.indices(size)[0] + x.indices(size)[2] <= size
+            and x.indices(size)[0] + x.indices(size)[2] >= -1
+        )
+        or x.start == x.stop,
+    )
+
+
+@use_several_sizes
+def test_step_will_not_be_zero(size):
+    assert_all_examples(st.slices(size), lambda x: x.step != 0)
+
+
+@use_several_sizes
+def test_slices_will_shrink(size):
+    sliced = minimal(st.slices(size))
+    assert sliced.start == 0 or sliced.start is None
+    assert sliced.stop == 0 or sliced.stop is None
+    assert sliced.step == 1
+
+
+@given(st.integers(1, 1000))
+@settings(deadline=None)
+def test_step_will_be_negative(size):
+    find_any(st.slices(size), lambda x: x.step < 0)
+
+
+@given(st.integers(1, 1000))
+@settings(deadline=None)
+def test_step_will_be_positive(size):
+    find_any(st.slices(size), lambda x: x.step > 0)
+
+
+@given(st.integers(1, 10))
+@settings(deadline=None)
+def test_stop_will_equal_size(size):
+    find_any(st.slices(size), lambda x: x.stop == size)
+
+
+@given(st.integers(1, 10))
+@settings(deadline=None)
+def test_start_will_equal_size(size):
+    find_any(st.slices(size), lambda x: x.start == size - 1)
+
+
+@given(st.integers(1, 1000))
+@settings(deadline=None)
+def test_start_will_equal_0(size):
+    find_any(st.slices(size), lambda x: x.start == 0)
+
+
+@given(st.integers(1, 1000))
+@settings(deadline=None)
+def test_start_will_equal_stop(size):
+    find_any(st.slices(size), lambda x: x.start == x.stop)