Skip to content

Commit e1b400b

Browse files
authored
Merge pull request #3275 from honno/parametrize-xp-tests
2 parents c1e0872 + 6b22648 commit e1b400b

File tree

11 files changed

+609
-506
lines changed

11 files changed

+609
-506
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch simplifies the repr of the strategies namespace returned in
4+
:func:`~hypothesis.extra.array_api.make_strategies_namespace`, e.g.
5+
6+
.. code-block:: pycon
7+
8+
>>> from hypothesis.extra.array_api import make_strategies_namespace
9+
>>> from numpy import array_api as xp
10+
>>> xps = make_strategies_namespace(xp)
11+
>>> xps
12+
make_strategies_namespace(numpy.array_api)
13+

hypothesis-python/src/hypothesis/extra/array_api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,11 @@ def floating_dtypes(
863863
unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__
864864
floating_dtypes.__doc__ = _floating_dtypes.__doc__
865865

866-
return SimpleNamespace(
866+
class PrettySimpleNamespace(SimpleNamespace):
867+
def __repr__(self):
868+
return f"make_strategies_namespace({xp.__name__})"
869+
870+
return PrettySimpleNamespace(
867871
from_dtype=from_dtype,
868872
arrays=arrays,
869873
array_shapes=array_shapes,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
This folder contains tests for `hypothesis.extra.array_api`.
2+
3+
## Running against different array modules
4+
5+
By default it will run against `numpy.array_api`. If that's not available
6+
(likely because an older NumPy version is installed), these tests will fallback
7+
to using the mock defined at the bottom of `src/hypothesis/extra/array_api.py`.
8+
9+
You can test other array modules which adopt the Array API via the
10+
`HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized
11+
options:
12+
13+
* `"default"`: only uses `numpy.array_api`, or if not available, fallbacks to the mock.
14+
* `"all"`: uses all array modules found via entry points, _and_ the mock.
15+
16+
If neither of these, the test suite will then try resolve the variable like so:
17+
18+
1. If the variable matches a name of an available entry point, load said entry point.
19+
2. If the variables matches a valid import path, import said path.
20+
21+
For example, to specify NumPy's Array API implementation, you could use its
22+
entry point (**1.**),
23+
24+
HYPOTHESIS_TEST_ARRAY_API=numpy pytest tests/array_api
25+
26+
or use the import path (**2.**),
27+
28+
HYPOTHESIS_TEST_ARRAY_API=numpy.array_api pytest tests/array_api
29+
30+
The former method is more ergonomic, but as entry points are optional for
31+
adopting the Array API, you will need to use the latter method for libraries
32+
that opt-out.

hypothesis-python/tests/array_api/common.py

Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,12 @@
1010

1111
from importlib.metadata import EntryPoint, entry_points # type: ignore
1212
from typing import Dict
13-
from warnings import catch_warnings
1413

15-
import pytest
16-
17-
from hypothesis.errors import HypothesisWarning
18-
from hypothesis.extra.array_api import make_strategies_namespace, mock_xp
1914
from hypothesis.internal.floats import next_up
2015

2116
__all__ = [
22-
"xp",
23-
"xps",
24-
"COMPLIANT_XP",
25-
"WIDTHS_FTZ",
17+
"installed_array_modules",
18+
"flushes_to_zero",
2619
]
2720

2821

@@ -44,25 +37,14 @@ def installed_array_modules() -> Dict[str, EntryPoint]:
4437
return {ep.name: ep for ep in eps}
4538

4639

47-
# We try importing the Array API namespace from NumPy first, which modern
48-
# versions should include. If not available we default to our own mocked module,
49-
# which should allow our test suite to still work. A constant is set accordingly
50-
# to inform our test suite of whether the array module here is a mock or not.
51-
modules = installed_array_modules()
52-
try:
53-
with catch_warnings(): # NumPy currently warns on import
54-
xp = modules["numpy"].load()
55-
except KeyError:
56-
xp = mock_xp
57-
with pytest.warns(HypothesisWarning):
58-
xps = make_strategies_namespace(xp)
59-
COMPLIANT_XP = False
60-
else:
61-
xps = make_strategies_namespace(xp)
62-
COMPLIANT_XP = True
40+
def flushes_to_zero(xp, width: int) -> bool:
41+
"""Infer whether build of array module has its float dtype of the specified
42+
width flush subnormals to zero
6343
64-
# Infer whether build of array module has its float flush subnormals to zero
65-
WIDTHS_FTZ = {
66-
32: bool(xp.asarray(next_up(0.0, width=32), dtype=xp.float32) == 0),
67-
64: bool(xp.asarray(next_up(0.0, width=64), dtype=xp.float64) == 0),
68-
}
44+
We do this per-width because compilers might FTZ for one dtype but allow
45+
subnormals in the other.
46+
"""
47+
if width not in [32, 64]:
48+
raise ValueError(f"{width=}, but should be either 32 or 64")
49+
dtype = getattr(xp, f"float{width}")
50+
return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# This file is part of Hypothesis, which may be found at
2+
# https://github.com/HypothesisWorks/hypothesis/
3+
#
4+
# Copyright the Hypothesis Authors.
5+
# Individual contributors are listed in AUTHORS.rst and the git log.
6+
#
7+
# This Source Code Form is subject to the terms of the Mozilla Public License,
8+
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9+
# obtain one at https://mozilla.org/MPL/2.0/.
10+
11+
import warnings
12+
from importlib import import_module
13+
from os import getenv
14+
15+
import pytest
16+
17+
from hypothesis.errors import HypothesisWarning
18+
from hypothesis.extra.array_api import make_strategies_namespace, mock_xp
19+
20+
from tests.array_api.common import installed_array_modules
21+
22+
with pytest.warns(HypothesisWarning):
23+
mock_xps = make_strategies_namespace(mock_xp)
24+
25+
# See README.md in regards to the HYPOTHESIS_TEST_ARRAY_API env variable
26+
test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default")
27+
name_to_entry_point = installed_array_modules()
28+
with warnings.catch_warnings():
29+
# We ignore all warnings here as many array modules warn on import
30+
warnings.simplefilter("ignore")
31+
# We go through the steps described in README.md to define `params`, which
32+
# contains the array module(s) to be ran against the test suite.
33+
# Specifically `params` is a list of pytest parameters, with each parameter
34+
# containing the array module and its respective strategies namespace.
35+
if test_xp_option == "default":
36+
try:
37+
xp = name_to_entry_point["numpy"].load()
38+
xps = make_strategies_namespace(xp)
39+
params = [pytest.param(xp, xps, id="numpy")]
40+
except KeyError:
41+
params = [pytest.param(mock_xp, mock_xps, id="mock")]
42+
elif test_xp_option == "all":
43+
if len(name_to_entry_point) == 0:
44+
raise ValueError(
45+
"HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found"
46+
)
47+
params = [pytest.param(mock_xp, mock_xps, id="mock")]
48+
for name, ep in name_to_entry_point.items():
49+
xp = ep.load()
50+
xps = make_strategies_namespace(xp)
51+
params.append(pytest.param(xp, xps, id=name))
52+
elif test_xp_option in name_to_entry_point.keys():
53+
ep = name_to_entry_point[test_xp_option]
54+
xp = ep.load()
55+
xps = make_strategies_namespace(xp)
56+
params = [pytest.param(xp, xps, id=test_xp_option)]
57+
else:
58+
try:
59+
xp = import_module(test_xp_option)
60+
xps = make_strategies_namespace(xp)
61+
params = [pytest.param(xp, xps, id=test_xp_option)]
62+
except ImportError as e:
63+
raise ValueError(
64+
f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid "
65+
"option ('default' or 'all'), name of an available entry point, "
66+
"or a valid import path."
67+
) from e
68+
69+
70+
def pytest_generate_tests(metafunc):
71+
if "xp" in metafunc.fixturenames and "xps" in metafunc.fixturenames:
72+
metafunc.parametrize("xp, xps", params)

0 commit comments

Comments
 (0)