Repository: HypothesisWorks/hypothesis

Issue:
numpy arrays() strategy does not respect (unsigned) dtype (anymore)
Since version `6.98.12` (and #3795/#3895) the numpy [`arrays()`](https://hypothesis.readthedocs.io/en/latest/numpy.html#hypothesis.extra.numpy.arrays) strategy seems to not respect the type of its `dtype` parameter anymore.

The documentation states for parameter `elements`
> `elements` is a strategy for generating values to put in the array. If it is None **a suitable value will be inferred based on the dtype**, which may give any legal value (including eg NaN for floats).

This seems to have stopped working in the recent releases for at least dtype `np.uint32`, so test data generation produces a lot of invalid arguments and often triggers the health check. Once you pass a strategy for valid values (e.g. `st.integers(min_value=0, max_value=2**32 - 1)`) test generation does work again.

To reproduce run the following as python file with `pytest -s` (you need numpy and pytest and a recent hypothesis installed). It will either let the first test fail (healthcheck triggers because of invalid samples) or be really slow. 

```python
import numpy as np
from hypothesis import given
from hypothesis import strategies as st
from hypothesis.extra.numpy import arrays

failing_count = 0
failing_runs = 0
working_count = 0
working_runs = 0


@st.composite
def failing_vector_of_even_length(draw, min_len=0, max_len=10000):
    global failing_runs
    failing_runs += 1

    even_1d_shape = draw(
        st.integers(min_value=min_len, max_value=max_len).filter(lambda x: x % 2 == 0)
    )
    array = draw(
        arrays(
            dtype=np.uint32,
            shape=even_1d_shape,
        )
    )
    global failing_count

    if array is not None:
        failing_count += 1
    return array


@st.composite
def vector_of_even_length(draw, min_len=0, max_len=10000):
    global working_runs
    working_runs += 1

    even_1d_shape = draw(
        st.integers(min_value=min_len, max_value=max_len).filter(lambda x: x % 2 == 0)
    )
    array = draw(
        arrays(
            dtype=np.uint32,
            shape=even_1d_shape,
            elements=st.integers(min_value=0, max_value=2**32 - 1),
        )
    )
    global working_count

    if array is not None:
        working_count += 1
    return array


@given(data=failing_vector_of_even_length())
def test_showcase(data):
    pass


@given(data=vector_of_even_length())
def test_showcase2(data):
    pass


def test_result():
    print(
        f"no elements: {failing_count} out of {failing_runs} worked; with elements: {working_count} out of {working_runs} worked."
    )
```


Discussion:
Thanks for the report - this is clearly a pretty serious bug, and shipping it implies that we also need to improve our tests for dtype inference.  I'll likely get to this over the weekend.

Resolving patch:
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
@@ -235,6 +235,12 @@ def __init__(self, element_strategy, shape, dtype, fill, unique):
         self.unique = unique
         self._check_elements = dtype.kind not in ("O", "V")
 
+    def __repr__(self):
+        return (
+            f"ArrayStrategy({self.element_strategy!r}, shape={self.shape}, "
+            f"dtype={self.dtype!r}, fill={self.fill!r}, unique={self.unique!r})"
+        )
+
     def set_element(self, val, result, idx, *, fill=False):
         try:
             result[idx] = val
@@ -547,6 +553,8 @@ def arrays(
     unwrapped = unwrap_strategies(elements)
     if isinstance(unwrapped, MappedStrategy) and unwrapped.pack == dtype.type:
         elements = unwrapped.mapped_strategy
+        if getattr(unwrapped, "force_has_reusable_values", False):
+            elements.force_has_reusable_values = True  # type: ignore
     if isinstance(shape, int):
         shape = (shape,)
     shape = tuple(shape)
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/collections.py b/hypothesis-python/src/hypothesis/strategies/_internal/collections.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/collections.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/collections.py
@@ -143,15 +143,18 @@ def __init__(self, elements, min_size=0, max_size=float("inf")):
         self.min_size = min_size or 0
         self.max_size = max_size if max_size is not None else float("inf")
         assert 0 <= self.min_size <= self.max_size
-        if min_size > BUFFER_SIZE:
-            raise InvalidArgument(
-                f"min_size={min_size:_d} is larger than Hypothesis is designed to handle"
-            )
         self.average_size = min(
             max(self.min_size * 2, self.min_size + 5),
             0.5 * (self.min_size + self.max_size),
         )
         self.element_strategy = elements
+        if min_size > BUFFER_SIZE:
+            raise InvalidArgument(
+                f"{self!r} can never generate an example, because min_size is larger "
+                "than Hypothesis supports.  Including it is at best slowing down your "
+                "tests for no benefit; at worst making them fail (maybe flakily) with "
+                "a HealthCheck error."
+            )
 
     def calc_label(self):
         return combine_labels(self.class_label, self.element_strategy.label)
@@ -193,7 +196,7 @@ def do_draw(self, data):
         return result
 
     def __repr__(self):
-        return "{}({!r}, min_size={!r}, max_size={!r})".format(
+        return "{}({!r}, min_size={:_}, max_size={:_})".format(
             self.__class__.__name__, self.element_strategy, self.min_size, self.max_size
         )
 


Test patch:
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
@@ -26,6 +26,7 @@
 )
 from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption
 from hypothesis.extra import numpy as nps
+from hypothesis.strategies._internal.lazy import unwrap_strategies
 
 from tests.common.debug import check_can_generate_examples, find_any, minimal
 from tests.common.utils import fails_with, flaky
@@ -1240,3 +1241,19 @@ def test_no_recursion_in_multi_line_reprs_issue_3560(data):
             dtype=float,
         ).map(lambda x: x)
     )
+
+
+def test_infers_elements_and_fill():
+    # Regression test for https://github.com/HypothesisWorks/hypothesis/issues/3900
+    # We only infer a fill strategy if the elements_strategy has reusable values,
+    # and the interaction of two performance fixes broke this.  Oops...
+    s = unwrap_strategies(nps.arrays(dtype=np.uint32, shape=1))
+    assert isinstance(s, nps.ArrayStrategy)
+    assert repr(s.element_strategy) == f"integers(0, {2**32-1})"
+    assert repr(s.fill) == f"integers(0, {2**32-1})"
+
+    # But we _don't_ infer a fill if the elements strategy is non-reusable
+    elems = st.builds(lambda x: x * 2, st.integers(1, 10)).map(np.uint32)
+    assert not elems.has_reusable_values
+    s = unwrap_strategies(nps.arrays(dtype=np.uint32, shape=1, elements=elems))
+    assert s.fill.is_empty