Repository: HypothesisWorks/hypothesis

Issue:
Test for compatibility with Numpy prerelease (2.0 is coming this month)
https://github.com/pydata/xarray/issues/8844#issuecomment-2048538094 points out that [this test](https://github.com/pydata/xarray/blob/07c7f969fe879af7a871374209957d5cd1ddb5aa/xarray/tests/test_strategies.py#L202-L222) is [failing](https://github.com/pydata/xarray/actions/runs/8638322254/job/23682526243?pr=8854#step:8:2530) on the [upcoming Numpy 2.0](https://github.com/numpy/numpy/issues/24300), and it looks like we're incompatible.

Specifically, `hypothesis.extra.array_api.make_strategies_namespace()` eventually calls `next_up()` on an instance of `numpy.float32`, and makes the failing asserting that this is an instance of builtin `float`.

- [ ] [Add a numpy-prerelease build to our CI system](https://numpy.org/devdocs/dev/depending_on_numpy.html#testing-against-the-numpy-main-branch-or-pre-releases); we can allow failures (as for pyodide) but it's good to know
- [ ] If that doesn't reproduce this problem, add a test which does
- [ ] Fix the incompatibility, so that we'll work out of the box when Numpy 2.0 is released


Discussion:
potential test case:
```python
from hypothesis import given
from hypothesis.extra.array_api import make_strategies_namespace
import numpy as np

try:
    import numpy.array_api
    xps = make_strategies_namespace(numpy.array_api)
except ImportError:
    xps = make_strategies_namespace(np)

dtypes = xps.floating_dtypes()
shapes = xps.array_shapes()
arrays = xps.arrays(dtype=dtypes, shape=shapes)


@given(arrays)
def test_array_strategy(arr):
    # or even just `pass`
    assert hasattr(arr, "dtype")
```

as [pointed out](https://github.com/pydata/xarray/issues/8844#issuecomment-2051276145) by @seberg, the cause might be the changed casting rules in `numpy` 2.0, where before `float32(3) + 0.0` would be casted to `float64` (and thus pass the check) while now the result is a `float32`.

Resolving patch:
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
@@ -282,7 +282,7 @@ def check_valid_minmax(prefix, val, info_obj):
         if allow_subnormal is not None:
             kw["allow_subnormal"] = allow_subnormal
         else:
-            subnormal = next_down(finfo.smallest_normal, width=finfo.bits)
+            subnormal = next_down(float(finfo.smallest_normal), width=finfo.bits)
             ftz = bool(xp.asarray(subnormal, dtype=dtype) == 0)
             if ftz:
                 kw["allow_subnormal"] = False
@@ -303,7 +303,7 @@ def check_valid_minmax(prefix, val, info_obj):
         # complex array, in case complex arrays have different FTZ behaviour
         # than arrays of the respective composite float.
         if allow_subnormal is None:
-            subnormal = next_down(finfo.smallest_normal, width=finfo.bits)
+            subnormal = next_down(float(finfo.smallest_normal), width=finfo.bits)
             x = xp.asarray(complex(subnormal, subnormal), dtype=dtype)
             builtin_x = complex(x)
             allow_subnormal = builtin_x.real != 0 and builtin_x.imag != 0
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
@@ -908,9 +908,9 @@ def _collection_ish_functions():
             np.diag,
             # bonus undocumented functions from tab-completion:
             np.asarray_chkfinite,
-            np.asfarray,
             np.asfortranarray,
         ]
+
     return funcs
 
 
diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py
--- a/tooling/src/hypothesistooling/__main__.py
+++ b/tooling/src/hypothesistooling/__main__.py
@@ -502,6 +502,7 @@ def standard_tox_task(name, py=ci_version):
     standard_tox_task(f"crosshair-{kind}")
 
 standard_tox_task("py38-oldestnumpy", py="3.8")
+standard_tox_task("numpy-nightly", py="3.12")
 
 standard_tox_task("coverage")
 standard_tox_task("conjecture-coverage")


Test patch:
diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py
--- a/hypothesis-python/tests/array_api/conftest.py
+++ b/hypothesis-python/tests/array_api/conftest.py
@@ -35,7 +35,8 @@
         f"HYPOTHESIS_TEST_ARRAY_API_VERSION='{test_version_option}' is not "
         f"'default' or a valid api_version {NOMINAL_VERSIONS}."
     )
-with pytest.warns(HypothesisWarning):
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=HypothesisWarning)
     mock_version = "draft" if test_version_option == "default" else test_version_option
     mock_xps = make_strategies_namespace(mock_xp, api_version=mock_version)
 api_version = None if test_version_option == "default" else test_version_option
diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py
--- a/hypothesis-python/tests/array_api/test_arrays.py
+++ b/hypothesis-python/tests/array_api/test_arrays.py
@@ -8,6 +8,8 @@
 # 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/.
 
+import sys
+
 import pytest
 
 from hypothesis import given, settings, strategies as st
@@ -296,6 +298,25 @@ def test_may_not_fill_unique_array_with_non_nan(xp, xps):
         check_can_generate_examples(strat)
 
 
+@pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason="no complex")
+def test_floating_point_array():
+    import warnings
+    from hypothesis.extra.array_api import make_strategies_namespace
+
+    try:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            import numpy.array_api as nxp
+    except ModuleNotFoundError:
+        import numpy as nxp
+    xps = make_strategies_namespace(nxp)
+    dtypes = xps.floating_dtypes() | xps.complex_dtypes()
+
+    strat = xps.arrays(dtype=dtypes, shape=10)
+
+    check_can_generate_examples(strat)
+
+
 @pytest.mark.parametrize(
     "kwargs",
     [
diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py
--- a/hypothesis-python/tests/array_api/test_partial_adoptors.py
+++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py
@@ -11,6 +11,8 @@
 from copy import copy
 from types import SimpleNamespace
 from typing import Tuple
+import functools
+import warnings
 
 import pytest
 
@@ -31,17 +33,73 @@
 MOCK_WARN_MSG = f"determine.*{mock_xp.__name__}.*Array API"
 
 
-def make_mock_xp(*, exclude: Tuple[str, ...] = ()) -> SimpleNamespace:
+class MockedArray:
+    def __init__(self, wrapped, *, exclude=()):
+        self.wrapped = wrapped
+        self.exclude = exclude
+
+    def __getattr__(self, name):
+        if name in self.exclude:
+            raise AttributeError(f"removed on the mock: {name}")
+
+        return object.__getattr__(self, name)
+
+
+def wrap_array(func: callable, exclude: Tuple[str, ...] = ()) -> callable:
+    @functools.wraps(func)
+    def wrapped(*args, **kwargs):
+        result = func(*args, **kwargs)
+
+        if isinstance(result, tuple):
+            return tuple(MockedArray(arr, exclude=exclude) for arr in result)
+
+        return MockedArray(result, exclude=exclude)
+
+    return wrapped
+
+
+def make_mock_xp(
+    *, exclude: Tuple[str, ...] = (), exclude_methods: Tuple[str, ...] = ()
+) -> SimpleNamespace:
     xp = copy(mock_xp)
     assert isinstance(exclude, tuple)  # sanity check
+    assert isinstance(exclude_methods, tuple)  # sanity check
     for attr in exclude:
         delattr(xp, attr)
+
+    array_returning_funcs = (
+        "astype",
+        "broadcast_arrays",
+        "arange",
+        "asarray",
+        "empty",
+        "zeros",
+        "ones",
+        "reshape",
+        "isnan",
+        "isfinite",
+        "logical_or",
+        "sum",
+        "nonzero",
+        "sort",
+        "unique_values",
+        "any",
+        "all",
+    )
+
+    for name in array_returning_funcs:
+        func = getattr(xp, name, None)
+        if func is None:
+            # removed in the step before
+            continue
+        setattr(xp, name, wrap_array(func, exclude=exclude_methods))
+
     return xp
 
 
 def test_warning_on_noncompliant_xp():
     """Using non-compliant array modules raises helpful warning"""
-    xp = make_mock_xp()
+    xp = make_mock_xp(exclude_methods=("__array_namespace__",))
     with pytest.warns(HypothesisWarning, match=MOCK_WARN_MSG):
         make_strategies_namespace(xp, api_version="draft")
 
@@ -62,7 +120,8 @@ def test_error_on_missing_attr(stratname, args, attr):
 
 
 dtypeless_xp = make_mock_xp(exclude=tuple(DTYPE_NAMES))
-with pytest.warns(HypothesisWarning):
+with warnings.catch_warnings():
+    warnings.filterwarnings("ignore", category=HypothesisWarning)
     dtypeless_xps = make_strategies_namespace(dtypeless_xp, api_version="draft")
 
 
diff --git a/hypothesis-python/tests/numpy/test_from_dtype.py b/hypothesis-python/tests/numpy/test_from_dtype.py
--- a/hypothesis-python/tests/numpy/test_from_dtype.py
+++ b/hypothesis-python/tests/numpy/test_from_dtype.py
@@ -21,6 +21,8 @@
 
 from tests.common.debug import assert_no_examples, check_can_generate_examples, find_any
 
+np_version = tuple(int(x) for x in np.__version__.split(".")[:2])
+
 STANDARD_TYPES = [
     np.dtype(t)
     for t in (
@@ -126,7 +128,12 @@ def test_byte_string_dtypes_generate_unicode_strings(data):
     assert isinstance(result, bytes)
 
 
-@pytest.mark.parametrize("dtype", ["U", "S", "a"])
+skipif_np2 = pytest.mark.skipif(np_version >= (2, 0), reason="removed in new version")
+
+@pytest.mark.parametrize(
+    "dtype",
+    ["U", "S", pytest.param("a", marks=skipif_np2)],
+)
 def test_unsized_strings_length_gt_one(dtype):
     # See https://github.com/HypothesisWorks/hypothesis/issues/2229
     find_any(nps.arrays(dtype=dtype, shape=1), lambda arr: len(arr[0]) >= 2)
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
@@ -348,7 +348,11 @@ def test_may_not_fill_with_non_nan_when_unique_is_set_and_type_is_not_number(arr
 
 @pytest.mark.parametrize("fill", [False, True])
 # Overflowing elements deprecated upstream in Numpy 1.24 :-)
-@fails_with(InvalidArgument if np_version < (1, 24) else DeprecationWarning)
+@fails_with(
+    InvalidArgument
+    if np_version < (1, 24)
+    else (DeprecationWarning if np_version < (2, 0) else OverflowError)
+)
 @given(st.data())
 def test_overflowing_integers_are_deprecated(fill, data):
     kw = {"elements": st.just(300)}