From b51512f446ab65d3fa091a32b724830a449df3d9 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 6 Sep 2022 10:42:09 +0100 Subject: [PATCH 01/62] Rudimentary xps versioning and complex support --- .../src/hypothesis/extra/array_api.py | 130 ++++++++++++++++-- hypothesis-python/tests/array_api/conftest.py | 10 +- .../tests/array_api/test_partial_adoptors.py | 15 +- .../tests/array_api/test_pretty.py | 6 +- .../tests/array_api/test_scalar_dtypes.py | 12 ++ 5 files changed, 148 insertions(+), 25 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 0b849d89b2..093ea760d1 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -17,6 +17,7 @@ Iterable, Iterator, List, + Literal, Mapping, NamedTuple, Optional, @@ -62,11 +63,23 @@ ] +# Be sure to keep versions in ascending order so api_verson_gt() works +RELEASED_VERSIONS = ("2021.12",) +NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) +NominalVersion = Literal[tuple(NOMINAL_VERSIONS)] + + +def api_verson_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: + return NOMINAL_VERSIONS.index(api_version1) > NOMINAL_VERSIONS.index(api_version2) + + INT_NAMES = ("int8", "int16", "int32", "int64") UINT_NAMES = ("uint8", "uint16", "uint32", "uint64") ALL_INT_NAMES = INT_NAMES + UINT_NAMES FLOAT_NAMES = ("float32", "float64") -NUMERIC_NAMES = ALL_INT_NAMES + FLOAT_NAMES +REAL_NAMES = ALL_INT_NAMES + FLOAT_NAMES +COMPLEX_NAMES = ("complex64", "complex128") +NUMERIC_NAMES = REAL_NAMES + COMPLEX_NAMES DTYPE_NAMES = ("bool",) + NUMERIC_NAMES DataType = TypeVar("DataType") @@ -107,7 +120,7 @@ def warn_on_missing_dtypes(xp: Any, stubs: List[str]) -> None: def find_castable_builtin_for_dtype( xp: Any, dtype: DataType -) -> Type[Union[bool, int, float]]: +) -> Type[Union[bool, int, float, complex]]: """Returns builtin type which can have values that are castable to the given dtype, according to :xp-ref:`type promotion rules `. @@ -134,8 +147,13 @@ def find_castable_builtin_for_dtype( if dtype is not None and dtype in float_dtypes: return float + complex_dtypes, complex_stubs = partition_attributes_and_stubs(xp, COMPLEX_NAMES) + if dtype in complex_dtypes: + return complex + stubs.extend(int_stubs) stubs.extend(float_stubs) + stubs.extend(complex_stubs) if len(stubs) > 0: warn_on_missing_dtypes(xp, stubs) raise InvalidArgument(f"dtype={dtype} not recognised in {xp.__name__}") @@ -218,7 +236,7 @@ def check_valid_minmax(prefix, val, info_obj): check_valid_minmax("max", max_value, iinfo) check_valid_interval(min_value, max_value, "min_value", "max_value") return st.integers(min_value=min_value, max_value=max_value) - else: + elif builtin is float: finfo = xp.finfo(dtype) kw = {} @@ -269,6 +287,21 @@ def check_valid_minmax(prefix, val, info_obj): kw["exclude_max"] = exclude_max return st.floats(width=finfo.bits, **kw) + else: + try: + float32 = xp.float32 + float64 = xp.float64 + complex64 = xp.complex64 + complex128 = xp.complex64 + except AttributeError: + raise NotImplementedError() from e # TODO + else: + if dtype == complex64: + floats = _from_dtype(xp, float32) + else: + floats = _from_dtype(xp, float64) + + return st.builds(complex, floats, floats) class ArrayStrategy(st.SearchStrategy): @@ -548,9 +581,9 @@ def check_dtypes(xp: Any, dtypes: List[DataType], stubs: List[str]) -> None: warn_on_missing_dtypes(xp, stubs) -def _scalar_dtypes(xp: Any) -> st.SearchStrategy[DataType]: +def _scalar_dtypes(xp: Any, api_version: NominalVersion) -> st.SearchStrategy[DataType]: """Return a strategy for all :xp-ref:`valid dtype ` objects.""" - return st.one_of(_boolean_dtypes(xp), _numeric_dtypes(xp)) + return st.one_of(_boolean_dtypes(xp), _numeric_dtypes(xp, api_version)) def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]: @@ -563,8 +596,8 @@ def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]: ) from None -def _numeric_dtypes(xp: Any) -> st.SearchStrategy[DataType]: - """Return a strategy for all numeric dtype objects.""" +def _real_dtypes(xp: Any) -> st.SearchStrategy[DataType]: + """Return a strategy for all real dtype objects.""" return st.one_of( _integer_dtypes(xp), _unsigned_integer_dtypes(xp), @@ -572,6 +605,16 @@ def _numeric_dtypes(xp: Any) -> st.SearchStrategy[DataType]: ) +def _numeric_dtypes( + xp: Any, api_version: NominalVersion +) -> st.SearchStrategy[DataType]: + """Return a strategy for all numeric dtype objects.""" + strat = _real_dtypes(xp) + if api_verson_gt(api_version, "2021.12"): + strat |= _complex_dtypes(xp) + return strat + + @check_function def check_valid_sizes( category: str, sizes: Sequence[int], valid_sizes: Sequence[int] @@ -649,6 +692,24 @@ def _floating_dtypes( return st.sampled_from(dtypes) +def _complex_dtypes( + xp: Any, *, sizes: Union[int, Sequence[int]] = (64, 128) +) -> st.SearchStrategy[DataType]: + """Return a strategy for complex dtype objects. + + ``sizes`` contains the complex sizes in bits, defaulting to ``(64, 128)`` + which covers all valid sizes. + """ + if isinstance(sizes, int): + sizes = (sizes,) + check_valid_sizes("complex", sizes, (64, 128)) + dtypes, stubs = partition_attributes_and_stubs( + xp, numeric_dtype_names("complex", sizes) + ) + check_dtypes(xp, dtypes, stubs) + return st.sampled_from(dtypes) + + @proxies(_valid_tuple_axes) def valid_tuple_axes(*args, **kwargs): return _valid_tuple_axes(*args, **kwargs) @@ -760,10 +821,13 @@ def indices( ) -def make_strategies_namespace(xp: Any) -> SimpleNamespace: +def make_strategies_namespace( + xp: Any, *, api_version: Union["Ellipsis", NominalVersion] = ... +) -> SimpleNamespace: """Creates a strategies namespace for the given array module. * ``xp`` is the Array API library to automatically pass to the namespaced methods. + * ``api_version`` TODO A :obj:`python:types.SimpleNamespace` is returned which contains all the strategy methods in this module but without requiring the ``xp`` argument. @@ -778,12 +842,29 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace: >>> x Array([[-8, 6, 3], [-6, 4, 6]], dtype=int8) - >>> x.__array_namespace__() is xp + >>> x.__array_namespace__() is xp TODO True """ + array = xp.zeros(1) + if isinstance(api_version, str): + assert api_version in NOMINAL_VERSIONS # TODO + else: + if api_version is None: + raise ValueError("TODO") + assert api_version == Ellipsis # TODO + for api_version in ["2021.12"]: + try: + xp = array.__array_namespace__(api_version=api_version) + except Exception: + pass + else: + break # xp and api_version kept TODO comment + else: + raise ValueError("TODO") + + array = xp.zeros(1) try: - array = xp.zeros(1) array.__array_namespace__() except Exception: warn( @@ -837,15 +918,19 @@ def arrays( @defines_strategy() def scalar_dtypes() -> st.SearchStrategy[DataType]: - return _scalar_dtypes(xp) + return _scalar_dtypes(xp, api_version) @defines_strategy() def boolean_dtypes() -> st.SearchStrategy[DataType]: return _boolean_dtypes(xp) + @defines_strategy() + def real_dtypes() -> st.SearchStrategy[DataType]: + return _real_dtypes(xp) + @defines_strategy() def numeric_dtypes() -> st.SearchStrategy[DataType]: - return _numeric_dtypes(xp) + return _numeric_dtypes(xp, api_version) @defines_strategy() def integer_dtypes( @@ -869,6 +954,7 @@ def floating_dtypes( arrays.__doc__ = _arrays.__doc__ scalar_dtypes.__doc__ = _scalar_dtypes.__doc__ boolean_dtypes.__doc__ = _boolean_dtypes.__doc__ + real_dtypes.__doc__ = _real_dtypes.__doc__ numeric_dtypes.__doc__ = _numeric_dtypes.__doc__ integer_dtypes.__doc__ = _integer_dtypes.__doc__ unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__ @@ -876,14 +962,15 @@ def floating_dtypes( class PrettySimpleNamespace(SimpleNamespace): def __repr__(self): - return f"make_strategies_namespace({xp.__name__})" + return f"make_strategies_namespace({xp.__name__}, {api_version=})" - return PrettySimpleNamespace( + kwargs = dict( from_dtype=from_dtype, arrays=arrays, array_shapes=array_shapes, scalar_dtypes=scalar_dtypes, boolean_dtypes=boolean_dtypes, + real_dtypes=real_dtypes, numeric_dtypes=numeric_dtypes, integer_dtypes=integer_dtypes, unsigned_integer_dtypes=unsigned_integer_dtypes, @@ -894,6 +981,19 @@ def __repr__(self): indices=indices, ) + if api_verson_gt(api_version, "2021.12"): + + @defines_strategy() + def complex_dtypes( + *, sizes: Union[int, Sequence[int]] = (64, 128) + ) -> st.SearchStrategy[DataType]: + return _complex_dtypes(xp, sizes=sizes) + + complex_dtypes.__doc__ = _complex_dtypes.__doc__ + kwargs["complex_dtypes"] = complex_dtypes + + return PrettySimpleNamespace(**kwargs) + try: import numpy as np @@ -947,6 +1047,8 @@ def mock_finfo(dtype: DataType) -> FloatInfo: uint64=np.uint64, float32=np.float32, float64=np.float64, + complex64=np.complex64, + complex128=np.complex128, bool=np.bool_, # Constants nan=np.nan, diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index afa59d40a8..33a7794fbe 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -20,7 +20,7 @@ from tests.array_api.common import installed_array_modules with pytest.warns(HypothesisWarning): - mock_xps = make_strategies_namespace(mock_xp) + mock_xps = make_strategies_namespace(mock_xp, api_version="draft") # See README.md in regards to the HYPOTHESIS_TEST_ARRAY_API env variable test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default") @@ -35,7 +35,7 @@ if test_xp_option == "default": try: xp = name_to_entry_point["numpy"].load() - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") params = [pytest.param(xp, xps, id="numpy")] except KeyError: params = [pytest.param(mock_xp, mock_xps, id="mock")] @@ -47,17 +47,17 @@ params = [pytest.param(mock_xp, mock_xps, id="mock")] for name, ep in name_to_entry_point.items(): xp = ep.load() - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") params.append(pytest.param(xp, xps, id=name)) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") params = [pytest.param(xp, xps, id=test_xp_option)] else: try: xp = import_module(test_xp_option) - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") params = [pytest.param(xp, xps, id=test_xp_option)] except ImportError as e: raise ValueError( diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index 6ae47a220d..0cdc1ee3ab 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -18,6 +18,7 @@ from hypothesis import given, strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra.array_api import ( + COMPLEX_NAMES, DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, @@ -41,7 +42,7 @@ def test_warning_on_noncompliant_xp(): """Using non-compliant array modules raises helpful warning""" xp = make_mock_xp() with pytest.warns(HypothesisWarning, match=MOCK_WARN_MSG): - make_strategies_namespace(xp) + make_strategies_namespace(xp, api_version="draft") @pytest.mark.filterwarnings(f"ignore:.*{MOCK_WARN_MSG}.*") @@ -53,7 +54,7 @@ def test_error_on_missing_attr(stratname, args, attr): """Strategies raise helpful error when using array modules that lack required attributes.""" xp = make_mock_xp(exclude=(attr,)) - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") func = getattr(xps, stratname) with pytest.raises(InvalidArgument, match=f"{mock_xp.__name__}.*required.*{attr}"): func(*args).example() @@ -61,7 +62,7 @@ def test_error_on_missing_attr(stratname, args, attr): dtypeless_xp = make_mock_xp(exclude=tuple(DTYPE_NAMES)) with pytest.warns(HypothesisWarning): - dtypeless_xps = make_strategies_namespace(dtypeless_xp) + dtypeless_xps = make_strategies_namespace(dtypeless_xp, api_version="draft") @pytest.mark.parametrize( @@ -73,6 +74,8 @@ def test_error_on_missing_attr(stratname, args, attr): "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", + "real_dtypes", + "complex_dtypes", ], ) def test_error_on_missing_dtypes(stratname): @@ -88,10 +91,12 @@ def test_error_on_missing_dtypes(stratname): "stratname, keep_anys", [ ("scalar_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]), - ("numeric_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]), + ("numeric_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES, COMPLEX_NAMES]), ("integer_dtypes", [INT_NAMES]), ("unsigned_integer_dtypes", [UINT_NAMES]), ("floating_dtypes", [FLOAT_NAMES]), + ("real_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]), + ("complex_dtypes", [COMPLEX_NAMES]), ], ) @given(st.data()) @@ -111,7 +116,7 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data): ) ) xp = make_mock_xp(exclude=tuple(exclude)) - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version="draft") func = getattr(xps, stratname) with pytest.warns(HypothesisWarning, match=f"{mock_xp.__name__}.*dtype.*namespace"): data.draw(func()) diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py index 79a7c6bb60..051373d7f0 100644 --- a/hypothesis-python/tests/array_api/test_pretty.py +++ b/hypothesis-python/tests/array_api/test_pretty.py @@ -25,6 +25,8 @@ "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", + "real_dtypes", + "complex_dtypes", "valid_tuple_axes", "broadcastable_shapes", "mutually_broadcastable_shapes", @@ -55,6 +57,8 @@ def test_namespaced_methods_meta(xp, xps, name): ("integer_dtypes", []), ("unsigned_integer_dtypes", []), ("floating_dtypes", []), + ("real_dtypes", []), + ("complex_dtypes", []), ("valid_tuple_axes", [0]), ("broadcastable_shapes", [()]), ("mutually_broadcastable_shapes", [3]), @@ -72,6 +76,6 @@ def test_namespaced_strategies_repr(xp, xps, name, valid_args): def test_strategies_namespace_repr(xp, xps): """Strategies namespace has good repr.""" - expected = f"make_strategies_namespace({xp.__name__})" + expected = f"make_strategies_namespace({xp.__name__}, api_version='draft')" assert repr(xps) == expected assert str(xps) == expected diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index 4f733a1796..b2b8498fce 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -11,10 +11,12 @@ import pytest from hypothesis.extra.array_api import ( + COMPLEX_NAMES, DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, NUMERIC_NAMES, + REAL_NAMES, UINT_NAMES, ) @@ -52,6 +54,16 @@ def test_can_generate_floating_dtypes(xp, xps): assert_all_examples(xps.floating_dtypes(), lambda dtype: dtype in float_dtypes) +def test_can_generate_real_dtypes(xp, xps): + real_dtypes = [getattr(xp, name) for name in REAL_NAMES] + assert_all_examples(xps.real_dtypes(), lambda dtype: dtype in real_dtypes) + + +def test_can_generate_complex_dtypes(xp, xps): + complex_dtypes = [getattr(xp, name) for name in COMPLEX_NAMES] + assert_all_examples(xps.complex_dtypes(), lambda dtype: dtype in complex_dtypes) + + def test_minimise_scalar_dtypes(xp, xps): assert minimal(xps.scalar_dtypes()) == xp.bool From e08ce5a786657c3caae22889d3717e7c980c04cb Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 12 Sep 2022 10:22:56 +0100 Subject: [PATCH 02/62] Fix argument validation for complex dtypes --- hypothesis-python/src/hypothesis/extra/array_api.py | 13 +++++++++++-- .../tests/array_api/test_argument_validation.py | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 093ea760d1..15a342503b 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -296,10 +296,19 @@ def check_valid_minmax(prefix, val, info_obj): except AttributeError: raise NotImplementedError() from e # TODO else: + kw = { + "min_value": min_value, + "max_value": max_value, + "allow_nan": allow_nan, + "allow_infinity": allow_infinity, + "allow_subnormal": allow_subnormal, + "exclude_min": exclude_min, + "exclude_max": exclude_max, + } if dtype == complex64: - floats = _from_dtype(xp, float32) + floats = _from_dtype(xp, float32, **kw) else: - floats = _from_dtype(xp, float64) + floats = _from_dtype(xp, float64, **kw) return st.builds(complex, floats, floats) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 0ea07d20ce..ca987414da 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -41,8 +41,11 @@ def e(name, **kwargs): e("from_dtype", dtype="int8", max_value="not an int"), e("from_dtype", dtype="float32", min_value="not a float"), e("from_dtype", dtype="float32", max_value="not a float"), + e("from_dtype", dtype="complex64", min_value="not a float"), + e("from_dtype", dtype="complex64", max_value="not a float"), e("from_dtype", dtype="int8", min_value=10, max_value=5), e("from_dtype", dtype="float32", min_value=10, max_value=5), + e("from_dtype", dtype="complex64", min_value=10, max_value=5), e("from_dtype", dtype="int8", min_value=-999), e("from_dtype", dtype="int8", max_value=-999), e("from_dtype", dtype="int8", min_value=999), @@ -55,12 +58,18 @@ def e(name, **kwargs): e("from_dtype", dtype="float32", max_value=-4e38), e("from_dtype", dtype="float32", min_value=4e38), e("from_dtype", dtype="float32", max_value=4e38), + e("from_dtype", dtype="complex64", min_value=-4e38), + e("from_dtype", dtype="complex64", max_value=-4e38), + e("from_dtype", dtype="complex64", min_value=4e38), + e("from_dtype", dtype="complex64", max_value=4e38), e("integer_dtypes", sizes=()), e("integer_dtypes", sizes=(3,)), e("unsigned_integer_dtypes", sizes=()), e("unsigned_integer_dtypes", sizes=(3,)), e("floating_dtypes", sizes=()), e("floating_dtypes", sizes=(3,)), + e("complex_dtypes", sizes=()), + e("complex_dtypes", sizes=(3,)), e("valid_tuple_axes", ndim=-1), e("valid_tuple_axes", ndim=2, min_size=-1), e("valid_tuple_axes", ndim=2, min_size=3, max_size=10), From 8b6acafd416def0a879e2a2bab70c4ca03928ecf Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 12 Sep 2022 10:25:27 +0100 Subject: [PATCH 03/62] Fix `check_set_value()` for complex numbers with inf components --- hypothesis-python/src/hypothesis/extra/array_api.py | 3 +-- hypothesis-python/tests/array_api/test_arrays.py | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 15a342503b..7601426709 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -326,8 +326,7 @@ def __init__(self, xp, elements_strategy, dtype, shape, fill, unique): self.finfo = None if self.builtin is not float else xp.finfo(self.dtype) def check_set_value(self, val, val_0d, strategy): - finite = self.builtin is bool or self.xp.isfinite(val_0d) - if finite and self.builtin(val_0d) != val: + if val == val and self.builtin(val_0d) != val: if self.builtin is float: assert self.finfo is not None # for mypy try: diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 6d9feff3d6..26c75c0cf9 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -296,6 +296,10 @@ def test_may_not_use_overflowing_integers(xp, xps, kwargs): [ ("float32", st.floats(min_value=10**40, allow_infinity=False)), ("float64", st.floats(min_value=10**40, allow_infinity=False)), + ( + "complex64", + st.complex_numbers(min_magnitude=10**300, allow_infinity=False), + ), ], ) def test_may_not_use_unrepresentable_elements(xp, xps, fill, dtype, strat): From 19d88a45908d959f3ddcde28cd8cc21cd32949ba Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 12 Sep 2022 11:08:58 +0100 Subject: [PATCH 04/62] Use check functions for `api_version` validation --- .../src/hypothesis/extra/array_api.py | 14 ++++++------- .../test_make_strategies_namespace.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 hypothesis-python/tests/array_api/test_make_strategies_namespace.py diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 7601426709..19e6ea3ce0 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -855,13 +855,13 @@ def make_strategies_namespace( """ array = xp.zeros(1) - if isinstance(api_version, str): - assert api_version in NOMINAL_VERSIONS # TODO - else: - if api_version is None: - raise ValueError("TODO") - assert api_version == Ellipsis # TODO - for api_version in ["2021.12"]: + check_argument( + api_version in NOMINAL_VERSIONS or api_version == Ellipsis, + f"{api_version=}, but api_version must be an ellipsis (...), or valid " + f"version string {RELEASED_VERSIONS}", + ) + if not isinstance(api_version, str): + for api_version in RELEASED_VERSIONS: try: xp = array.__array_namespace__(api_version=api_version) except Exception: diff --git a/hypothesis-python/tests/array_api/test_make_strategies_namespace.py b/hypothesis-python/tests/array_api/test_make_strategies_namespace.py new file mode 100644 index 0000000000..2b345a8cde --- /dev/null +++ b/hypothesis-python/tests/array_api/test_make_strategies_namespace.py @@ -0,0 +1,20 @@ +# This file is part of Hypothesis, which may be found at +# https://github.com/HypothesisWorks/hypothesis/ +# +# Copyright the Hypothesis Authors. +# Individual contributors are listed in AUTHORS.rst and the git log. +# +# 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/. + +import pytest + +from hypothesis.errors import InvalidArgument +from hypothesis.extra.array_api import make_strategies_namespace, mock_xp + + +@pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) +def test_raise_invalid_argument(api_version): + with pytest.raises(InvalidArgument): + make_strategies_namespace(mock_xp, api_version=api_version) From 94e025728b736550d148a709d77659079e490355 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 13 Sep 2022 13:07:32 +0100 Subject: [PATCH 05/62] Fix and test `api_version` inferrence --- .../src/hypothesis/extra/array_api.py | 20 ++++++-- .../array_api/test_argument_validation.py | 7 +++ .../tests/array_api/test_partial_adoptors.py | 48 ++++++++++++++++++- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 19e6ea3ce0..a22a092d44 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -854,23 +854,32 @@ def make_strategies_namespace( True """ - array = xp.zeros(1) check_argument( api_version in NOMINAL_VERSIONS or api_version == Ellipsis, f"{api_version=}, but api_version must be an ellipsis (...), or valid " f"version string {RELEASED_VERSIONS}", ) if not isinstance(api_version, str): - for api_version in RELEASED_VERSIONS: + # When api_version=..., we infer the most recent API version for which + # the passed xp is valid. We go through the released versions in + # descending order, passing them to x.__array_namespace__() until no + # errors are raised, thus inferring that specific api_version is + # supported. If errors are raised for all released versions, we raise + # our own useful error. + array = xp.zeros(1) + for api_version in reversed(RELEASED_VERSIONS): try: xp = array.__array_namespace__(api_version=api_version) except Exception: pass else: - break # xp and api_version kept TODO comment + break # i.e. a valid xp and api_version has been inferred else: - raise ValueError("TODO") - + raise InvalidArgument( + "Could not infer any api_version which module {xp.__name__} " + "supports. If you believe xp is indeed an Array API module, " + "try explicitly passing an api_version." + ) array = xp.zeros(1) try: array.__array_namespace__() @@ -973,6 +982,7 @@ def __repr__(self): return f"make_strategies_namespace({xp.__name__}, {api_version=})" kwargs = dict( + api_version=api_version, from_dtype=from_dtype, arrays=arrays, array_shapes=array_shapes, diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index ca987414da..99e626e2e0 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -11,6 +11,7 @@ import pytest from hypothesis.errors import InvalidArgument +from hypothesis.extra.array_api import make_strategies_namespace, mock_xp def e(name, **kwargs): @@ -223,3 +224,9 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs): strat = strat_func(**kwargs) with pytest.raises(InvalidArgument): strat.example() + + +@pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) +def test_make_namespace_raise_invalid_argument(api_version): + with pytest.raises(InvalidArgument): + make_strategies_namespace(mock_xp, api_version=api_version) diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index 0cdc1ee3ab..0824fb5c7e 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -11,7 +11,7 @@ from copy import copy from functools import lru_cache from types import SimpleNamespace -from typing import Tuple +from typing import List, Literal, Optional, Tuple import pytest @@ -22,6 +22,7 @@ DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, + RELEASED_VERSIONS, UINT_NAMES, make_strategies_namespace, mock_xp, @@ -120,3 +121,48 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data): func = getattr(xps, stratname) with pytest.warns(HypothesisWarning, match=f"{mock_xp.__name__}.*dtype.*namespace"): data.draw(func()) + + +class MockArray: + def __init__(self, supported_versions: Tuple[Literal[RELEASED_VERSIONS], ...]): + self.supported_versions = supported_versions + + def __array_namespace__(self, *, api_version: Optional[str] = None): + if api_version is not None and api_version not in self.supported_versions: + raise + return SimpleNamespace( + __name__="foopy", zeros=lambda _: MockArray(self.supported_versions) + ) + + +version_permutations: List[Tuple[Literal[RELEASED_VERSIONS], ...]] = [ + RELEASED_VERSIONS[:i] for i in range(1, len(RELEASED_VERSIONS) + 1) +] + + +@pytest.mark.parametrize( + "supported_versions", + version_permutations, + ids=lambda supported_versions: "-".join(supported_versions), +) +def test_version_inferrence(supported_versions): + xp = MockArray(supported_versions).__array_namespace__() + xps = make_strategies_namespace(xp) + assert xps.api_version == supported_versions[-1] + + +def test_raises_on_inferring_with_no_supported_versions(): + xp = MockArray(()).__array_namespace__() + with pytest.raises(InvalidArgument): + xps = make_strategies_namespace(xp) + + +@pytest.mark.parametrize( + ("api_version", "supported_versions"), + [pytest.param(p[-1], p[:-1], id=p[-1]) for p in version_permutations], +) +def test_warns_on_specifying_unsupported_version(api_version, supported_versions): + xp = MockArray(supported_versions).__array_namespace__() + with pytest.warns(HypothesisWarning): + xps = make_strategies_namespace(xp) + assert xps.api_version == api_version From 023d9017ca9dab573c1e07b5a100c22b3545143a Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 13 Sep 2022 13:24:14 +0100 Subject: [PATCH 06/62] Parametrize `xp` with most recent supporting `api_version` in tests --- hypothesis-python/tests/array_api/conftest.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 33a7794fbe..cd98ae0ea0 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -35,8 +35,8 @@ if test_xp_option == "default": try: xp = name_to_entry_point["numpy"].load() - xps = make_strategies_namespace(xp, api_version="draft") - params = [pytest.param(xp, xps, id="numpy")] + xps = make_strategies_namespace(xp) + params = [pytest.param(xp, xps, id=f"numpy-{xps.api_version}")] except KeyError: params = [pytest.param(mock_xp, mock_xps, id="mock")] elif test_xp_option == "all": @@ -44,21 +44,21 @@ raise ValueError( "HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found" ) - params = [pytest.param(mock_xp, mock_xps, id="mock")] + params = [pytest.param(mock_xp, mock_xps, id="mock-draft")] for name, ep in name_to_entry_point.items(): xp = ep.load() - xps = make_strategies_namespace(xp, api_version="draft") - params.append(pytest.param(xp, xps, id=name)) + xps = make_strategies_namespace(xp) + params.append(pytest.param(xp, xps, id=f"{name}-{xps.api_version}")) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() - xps = make_strategies_namespace(xp, api_version="draft") - params = [pytest.param(xp, xps, id=test_xp_option)] + xps = make_strategies_namespace(xp) + params = [pytest.param(xp, xps, id=f"{test_xp_option}-{xps.api_version}")] else: try: xp = import_module(test_xp_option) - xps = make_strategies_namespace(xp, api_version="draft") - params = [pytest.param(xp, xps, id=test_xp_option)] + xps = make_strategies_namespace(xp) + params = [pytest.param(xp, xps, id=f"{test_xp_option}-{xps.api_version}")] except ImportError as e: raise ValueError( f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid " From a7a2ed025383a482e573f39439c00309b1ae19d4 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 14 Sep 2022 18:00:02 +0100 Subject: [PATCH 07/62] Raise helpful error when inferring `api_version` when no `zeros()` --- .../src/hypothesis/extra/array_api.py | 23 +++++++++++++------ .../tests/array_api/test_partial_adoptors.py | 18 +++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index a22a092d44..7cb1afdd8a 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -866,7 +866,20 @@ def make_strategies_namespace( # errors are raised, thus inferring that specific api_version is # supported. If errors are raised for all released versions, we raise # our own useful error. - array = xp.zeros(1) + check_argument( + hasattr(xp, "zeros"), + f"Array module {xp.__name__} has no function zeros(), which is " + "required when inferring api_version.", + ) + errmsg = ( + f"Could not infer any api_version which module {xp.__name__} " + f"supports. If you believe {xp.__name__} is indeed an Array API " + "module, try explicitly passing an api_version." + ) + try: + array = xp.zeros(1) + except Exception: + raise InvalidArgument(errmsg) for api_version in reversed(RELEASED_VERSIONS): try: xp = array.__array_namespace__(api_version=api_version) @@ -875,13 +888,9 @@ def make_strategies_namespace( else: break # i.e. a valid xp and api_version has been inferred else: - raise InvalidArgument( - "Could not infer any api_version which module {xp.__name__} " - "supports. If you believe xp is indeed an Array API module, " - "try explicitly passing an api_version." - ) - array = xp.zeros(1) + raise InvalidArgument(errmsg) try: + array = xp.zeros(1) array.__array_namespace__() except Exception: warn( diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index 0824fb5c7e..d9781fdf99 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -32,8 +32,9 @@ @lru_cache() -def make_mock_xp(exclude: Tuple[str, ...] = ()) -> SimpleNamespace: +def make_mock_xp(*, exclude: Tuple[str, ...] = ()) -> SimpleNamespace: xp = copy(mock_xp) + assert isinstance(exclude, tuple) # sanity check for attr in exclude: delattr(xp, attr) return xp @@ -164,5 +165,18 @@ def test_raises_on_inferring_with_no_supported_versions(): def test_warns_on_specifying_unsupported_version(api_version, supported_versions): xp = MockArray(supported_versions).__array_namespace__() with pytest.warns(HypothesisWarning): - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version=api_version) assert xps.api_version == api_version + + +def test_raises_on_inferring_with_no_zeros_func(): + xp = make_mock_xp(exclude=("zeros",)) + with pytest.raises(InvalidArgument, match="has no function"): + xps = make_strategies_namespace(xp) + + +def test_raises_on_erroneous_zeros_func(): + xp = make_mock_xp() + setattr(xp, "zeros", None) + with pytest.raises(InvalidArgument): + xps = make_strategies_namespace(xp) From 18d1c703db3b8c11db24a76675349dfd8c8033eb Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 14 Sep 2022 18:03:28 +0100 Subject: [PATCH 08/62] More explicit `api_version` arg validation --- hypothesis-python/src/hypothesis/extra/array_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 7cb1afdd8a..02b72b8a01 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -855,11 +855,12 @@ def make_strategies_namespace( """ check_argument( - api_version in NOMINAL_VERSIONS or api_version == Ellipsis, + api_version == Ellipsis + or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), f"{api_version=}, but api_version must be an ellipsis (...), or valid " f"version string {RELEASED_VERSIONS}", ) - if not isinstance(api_version, str): + if api_version == Ellipsis: # When api_version=..., we infer the most recent API version for which # the passed xp is valid. We go through the released versions in # descending order, passing them to x.__array_namespace__() until no From 1684a15dee9c7f9224adaf37b3c395505ebb8801 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 14 Sep 2022 18:05:09 +0100 Subject: [PATCH 09/62] Remove unnecessary `tuple()` --- hypothesis-python/src/hypothesis/extra/array_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 02b72b8a01..f3a1c49a62 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -66,7 +66,7 @@ # Be sure to keep versions in ascending order so api_verson_gt() works RELEASED_VERSIONS = ("2021.12",) NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) -NominalVersion = Literal[tuple(NOMINAL_VERSIONS)] +NominalVersion = Literal[NOMINAL_VERSIONS] def api_verson_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: From a170115d5adb2769a9b7d3b43ff7f80341bd0e30 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 14 Sep 2022 18:16:00 +0100 Subject: [PATCH 10/62] Fix `test_warns_on_specifying_unsupported_version` --- hypothesis-python/tests/array_api/test_partial_adoptors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index d9781fdf99..350af87111 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -164,6 +164,7 @@ def test_raises_on_inferring_with_no_supported_versions(): ) def test_warns_on_specifying_unsupported_version(api_version, supported_versions): xp = MockArray(supported_versions).__array_namespace__() + xp.zeros = None with pytest.warns(HypothesisWarning): xps = make_strategies_namespace(xp, api_version=api_version) assert xps.api_version == api_version @@ -177,6 +178,6 @@ def test_raises_on_inferring_with_no_zeros_func(): def test_raises_on_erroneous_zeros_func(): xp = make_mock_xp() - setattr(xp, "zeros", None) + xp.zeros = None with pytest.raises(InvalidArgument): xps = make_strategies_namespace(xp) From 90662cf83dea93a819a5fce76228ed506e09f4da Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 14 Sep 2022 19:07:55 +0100 Subject: [PATCH 11/62] Delete redundant test file --- .../test_make_strategies_namespace.py | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 hypothesis-python/tests/array_api/test_make_strategies_namespace.py diff --git a/hypothesis-python/tests/array_api/test_make_strategies_namespace.py b/hypothesis-python/tests/array_api/test_make_strategies_namespace.py deleted file mode 100644 index 2b345a8cde..0000000000 --- a/hypothesis-python/tests/array_api/test_make_strategies_namespace.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file is part of Hypothesis, which may be found at -# https://github.com/HypothesisWorks/hypothesis/ -# -# Copyright the Hypothesis Authors. -# Individual contributors are listed in AUTHORS.rst and the git log. -# -# 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/. - -import pytest - -from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import make_strategies_namespace, mock_xp - - -@pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) -def test_raise_invalid_argument(api_version): - with pytest.raises(InvalidArgument): - make_strategies_namespace(mock_xp, api_version=api_version) From 387aa6004ecf4b934886cdf26a89f8935694fc69 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 15 Sep 2022 09:33:16 +0100 Subject: [PATCH 12/62] Remove erroneous and otherwise unused `complex128` var --- hypothesis-python/src/hypothesis/extra/array_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index f3a1c49a62..bd9dbf6a41 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -292,7 +292,6 @@ def check_valid_minmax(prefix, val, info_obj): float32 = xp.float32 float64 = xp.float64 complex64 = xp.complex64 - complex128 = xp.complex64 except AttributeError: raise NotImplementedError() from e # TODO else: From 858939d4a9b9baf415e9ba057540184671bd4422 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 10:29:18 +0100 Subject: [PATCH 13/62] Skip Array API tests requiring greater `api_version` --- hypothesis-python/tests/array_api/common.py | 10 ++++ hypothesis-python/tests/array_api/conftest.py | 32 +++++++++++- .../array_api/test_argument_validation.py | 51 ++++++++++++++----- .../tests/array_api/test_arrays.py | 15 +++--- .../tests/array_api/test_from_dtype.py | 12 ++--- .../tests/array_api/test_pretty.py | 4 +- .../tests/array_api/test_scalar_dtypes.py | 1 + 7 files changed, 98 insertions(+), 27 deletions(-) diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py index 72c674b435..4e60ad35ce 100644 --- a/hypothesis-python/tests/array_api/common.py +++ b/hypothesis-python/tests/array_api/common.py @@ -11,11 +11,15 @@ from importlib.metadata import EntryPoint, entry_points # type: ignore from typing import Dict +import pytest + +from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES from hypothesis.internal.floats import next_up __all__ = [ "installed_array_modules", "flushes_to_zero", + "dtype_name_params", ] @@ -48,3 +52,9 @@ def flushes_to_zero(xp, width: int) -> bool: raise ValueError(f"{width=}, but should be either 32 or 64") dtype = getattr(xp, f"float{width}") return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0) + + +dtype_name_params = ["bool"] + list(REAL_NAMES) +dtype_name_params += [ + pytest.param(n, marks=pytest.mark.xp_min_version("draft")) for n in COMPLEX_NAMES +] diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index cd98ae0ea0..08b64ecc78 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -11,11 +11,17 @@ import warnings from importlib import import_module from os import getenv +from typing import Literal import pytest from hypothesis.errors import HypothesisWarning -from hypothesis.extra.array_api import make_strategies_namespace, mock_xp +from hypothesis.extra.array_api import ( + RELEASED_VERSIONS, + api_verson_gt, + make_strategies_namespace, + mock_xp, +) from tests.array_api.common import installed_array_modules @@ -70,3 +76,27 @@ def pytest_generate_tests(metafunc): if "xp" in metafunc.fixturenames and "xps" in metafunc.fixturenames: metafunc.parametrize("xp, xps", params) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "xp_min_version(api_version): " + "mark test to run when greater or equal to api_version", + ) + + +def pytest_collection_modifyitems(config, items): + for item in items: + if all(f in item.fixturenames for f in ["xp", "xps"]): + try: + marker = next(m for m in item.own_markers if m.name == "xp_min_version") + except StopIteration: + pass + else: + item.callspec.params["xps"].api_version + min_version: Literal[RELEASED_VERSIONS] = marker.args[0] + if api_verson_gt(min_version, item.callspec.params["xps"].api_version): + item.add_marker( + pytest.mark.skip(reason=f"requires api_version=>{min_version}") + ) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 99e626e2e0..746dc43854 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -8,15 +8,26 @@ # 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/. +from typing import Optional + import pytest from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import make_strategies_namespace, mock_xp +from hypothesis.extra.array_api import ( + NominalVersion, + make_strategies_namespace, + mock_xp, +) -def e(name, **kwargs): +def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) - return pytest.param(name, kwargs, id=f"{name}({kw})") + id_ = f"{name}({kw})" + if _min_version is None: + marks = () + else: + marks = pytest.mark.xp_min_version(_min_version) + return pytest.param(name, kwargs, id=id_, marks=marks) @pytest.mark.parametrize( @@ -42,11 +53,27 @@ def e(name, **kwargs): e("from_dtype", dtype="int8", max_value="not an int"), e("from_dtype", dtype="float32", min_value="not a float"), e("from_dtype", dtype="float32", max_value="not a float"), - e("from_dtype", dtype="complex64", min_value="not a float"), - e("from_dtype", dtype="complex64", max_value="not a float"), + e( + "from_dtype", + _min_version="draft", + dtype="complex64", + min_value="not a float", + ), + e( + "from_dtype", + _min_version="draft", + dtype="complex64", + max_value="not a float", + ), e("from_dtype", dtype="int8", min_value=10, max_value=5), e("from_dtype", dtype="float32", min_value=10, max_value=5), - e("from_dtype", dtype="complex64", min_value=10, max_value=5), + e( + "from_dtype", + _min_version="draft", + dtype="complex64", + min_value=10, + max_value=5, + ), e("from_dtype", dtype="int8", min_value=-999), e("from_dtype", dtype="int8", max_value=-999), e("from_dtype", dtype="int8", min_value=999), @@ -59,18 +86,18 @@ def e(name, **kwargs): e("from_dtype", dtype="float32", max_value=-4e38), e("from_dtype", dtype="float32", min_value=4e38), e("from_dtype", dtype="float32", max_value=4e38), - e("from_dtype", dtype="complex64", min_value=-4e38), - e("from_dtype", dtype="complex64", max_value=-4e38), - e("from_dtype", dtype="complex64", min_value=4e38), - e("from_dtype", dtype="complex64", max_value=4e38), + e("from_dtype", _min_version="draft", dtype="complex64", min_value=-4e38), + e("from_dtype", _min_version="draft", dtype="complex64", max_value=-4e38), + e("from_dtype", _min_version="draft", dtype="complex64", min_value=4e38), + e("from_dtype", _min_version="draft", dtype="complex64", max_value=4e38), e("integer_dtypes", sizes=()), e("integer_dtypes", sizes=(3,)), e("unsigned_integer_dtypes", sizes=()), e("unsigned_integer_dtypes", sizes=(3,)), e("floating_dtypes", sizes=()), e("floating_dtypes", sizes=(3,)), - e("complex_dtypes", sizes=()), - e("complex_dtypes", sizes=(3,)), + e("complex_dtypes", _min_version="draft", sizes=()), + e("complex_dtypes", _min_version="draft", sizes=(3,)), e("valid_tuple_axes", ndim=-1), e("valid_tuple_axes", ndim=2, min_size=-1), e("valid_tuple_axes", ndim=2, min_size=3, max_size=10), diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 26c75c0cf9..2c6fda3421 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -12,10 +12,10 @@ from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import DTYPE_NAMES, NUMERIC_NAMES +from hypothesis.extra.array_api import DTYPE_NAMES from hypothesis.internal.floats import width_smallest_normals -from tests.array_api.common import flushes_to_zero +from tests.array_api.common import dtype_name_params, flushes_to_zero from tests.common.debug import assert_all_examples, find_any, minimal from tests.common.utils import flaky @@ -38,14 +38,14 @@ def xfail_on_indistinct_nans(xp): pytest.xfail("NaNs not distinct") -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_draw_arrays_from_dtype(xp, xps, dtype_name): """Draw arrays from dtypes.""" dtype = getattr(xp, dtype_name) assert_all_examples(xps.arrays(dtype, ()), lambda x: x.dtype == dtype) -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_draw_arrays_from_scalar_names(xp, xps, dtype_name): """Draw arrays from dtype names.""" dtype = getattr(xp, dtype_name) @@ -77,6 +77,8 @@ def test_draw_arrays_from_int_shapes(xp, xps, data): "integer_dtypes", "unsigned_integer_dtypes", "floating_dtypes", + "real_dtypes", + pytest.param("complex_dtypes", marks=pytest.mark.xp_min_version("draft")), ], ) def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name): @@ -156,7 +158,7 @@ def test_minimize_arrays_with_0d_shape_strategy(xp, xps): assert smallest.shape == () -@pytest.mark.parametrize("dtype", NUMERIC_NAMES) +@pytest.mark.parametrize("dtype", dtype_name_params[1:]) def test_minimizes_numeric_arrays(xp, xps, dtype): """Strategies with numeric dtypes minimize to zero-filled arrays.""" smallest = minimal(xps.arrays(dtype, (2, 2))) @@ -296,9 +298,10 @@ def test_may_not_use_overflowing_integers(xp, xps, kwargs): [ ("float32", st.floats(min_value=10**40, allow_infinity=False)), ("float64", st.floats(min_value=10**40, allow_infinity=False)), - ( + pytest.param( "complex64", st.complex_numbers(min_magnitude=10**300, allow_infinity=False), + marks=pytest.mark.xp_min_version("draft"), ), ], ) diff --git a/hypothesis-python/tests/array_api/test_from_dtype.py b/hypothesis-python/tests/array_api/test_from_dtype.py index 1ea61a5e2d..f45cf6d852 100644 --- a/hypothesis-python/tests/array_api/test_from_dtype.py +++ b/hypothesis-python/tests/array_api/test_from_dtype.py @@ -12,10 +12,10 @@ import pytest -from hypothesis.extra.array_api import DTYPE_NAMES, find_castable_builtin_for_dtype +from hypothesis.extra.array_api import find_castable_builtin_for_dtype from hypothesis.internal.floats import width_smallest_normals -from tests.array_api.common import flushes_to_zero +from tests.array_api.common import dtype_name_params, flushes_to_zero from tests.common.debug import ( assert_all_examples, assert_no_examples, @@ -24,14 +24,14 @@ ) -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_strategies_have_reusable_values(xp, xps, dtype_name): """Inferred strategies have reusable values.""" strat = xps.from_dtype(dtype_name) assert strat.has_reusable_values -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_produces_castable_instances_from_dtype(xp, xps, dtype_name): """Strategies inferred by dtype generate values of a builtin type castable to the dtype.""" @@ -40,7 +40,7 @@ def test_produces_castable_instances_from_dtype(xp, xps, dtype_name): assert_all_examples(xps.from_dtype(dtype), lambda v: isinstance(v, builtin)) -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_produces_castable_instances_from_name(xp, xps, dtype_name): """Strategies inferred by dtype name generate values of a builtin type castable to the dtype.""" @@ -49,7 +49,7 @@ def test_produces_castable_instances_from_name(xp, xps, dtype_name): assert_all_examples(xps.from_dtype(dtype_name), lambda v: isinstance(v, builtin)) -@pytest.mark.parametrize("dtype_name", DTYPE_NAMES) +@pytest.mark.parametrize("dtype_name", dtype_name_params) def test_passing_inferred_strategies_in_arrays(xp, xps, dtype_name): """Inferred strategies usable in arrays strategy.""" elements = xps.from_dtype(dtype_name) diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py index 051373d7f0..a9dfe010bc 100644 --- a/hypothesis-python/tests/array_api/test_pretty.py +++ b/hypothesis-python/tests/array_api/test_pretty.py @@ -26,7 +26,7 @@ "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", - "complex_dtypes", + pytest.param("complex_dtypes", marks=pytest.mark.xp_min_version("draft")), "valid_tuple_axes", "broadcastable_shapes", "mutually_broadcastable_shapes", @@ -58,7 +58,7 @@ def test_namespaced_methods_meta(xp, xps, name): ("unsigned_integer_dtypes", []), ("floating_dtypes", []), ("real_dtypes", []), - ("complex_dtypes", []), + pytest.param("complex_dtypes", [], marks=pytest.mark.xp_min_version("draft")), ("valid_tuple_axes", [0]), ("broadcastable_shapes", [()]), ("mutually_broadcastable_shapes", [3]), diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index b2b8498fce..407dd9cf84 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -59,6 +59,7 @@ def test_can_generate_real_dtypes(xp, xps): assert_all_examples(xps.real_dtypes(), lambda dtype: dtype in real_dtypes) +@pytest.mark.xp_min_version("draft") def test_can_generate_complex_dtypes(xp, xps): complex_dtypes = [getattr(xp, name) for name in COMPLEX_NAMES] assert_all_examples(xps.complex_dtypes(), lambda dtype: dtype in complex_dtypes) From 4784e05468d002282e52848c9ab8b543e6ab6f80 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 10:42:14 +0100 Subject: [PATCH 14/62] Change `test_can_generate_{scalar/numeric}_dtypes` on runtime --- .../tests/array_api/test_scalar_dtypes.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index 407dd9cf84..e9c2b3cc3d 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -18,13 +18,17 @@ NUMERIC_NAMES, REAL_NAMES, UINT_NAMES, + api_verson_gt, ) from tests.common.debug import assert_all_examples, find_any, minimal def test_can_generate_scalar_dtypes(xp, xps): - dtypes = [getattr(xp, name) for name in DTYPE_NAMES] + if api_verson_gt(xps.api_version, "2021.12"): + dtypes = [getattr(xp, name) for name in DTYPE_NAMES] + else: + dtypes = [getattr(xp, name) for name in ("bool",) + REAL_NAMES] assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes) @@ -33,7 +37,10 @@ def test_can_generate_boolean_dtypes(xp, xps): def test_can_generate_numeric_dtypes(xp, xps): - numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES] + if api_verson_gt(xps.api_version, "2021.12"): + numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES] + else: + numeric_dtypes = [getattr(xp, name) for name in REAL_NAMES] assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in numeric_dtypes) From 309a04906188c4752265defc87a87708e1c8de61 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 11:01:09 +0100 Subject: [PATCH 15/62] Fix `test_strategeis_namespace_repr` --- hypothesis-python/tests/array_api/test_pretty.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py index a9dfe010bc..0b544ad21c 100644 --- a/hypothesis-python/tests/array_api/test_pretty.py +++ b/hypothesis-python/tests/array_api/test_pretty.py @@ -76,6 +76,8 @@ def test_namespaced_strategies_repr(xp, xps, name, valid_args): def test_strategies_namespace_repr(xp, xps): """Strategies namespace has good repr.""" - expected = f"make_strategies_namespace({xp.__name__}, api_version='draft')" + expected = ( + f"make_strategies_namespace({xp.__name__}, api_version='{xps.api_version}')" + ) assert repr(xps) == expected assert str(xps) == expected From 0d344265a9e014f1b1048130a1736ab7406e7ba3 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 11:32:06 +0100 Subject: [PATCH 16/62] Pass `api_version` to `find_castable_builtin_for_dtype()` --- .../src/hypothesis/extra/array_api.py | 44 ++++++++++++------- .../tests/array_api/test_from_dtype.py | 4 +- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index bd9dbf6a41..23970b6520 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -119,7 +119,7 @@ def warn_on_missing_dtypes(xp: Any, stubs: List[str]) -> None: def find_castable_builtin_for_dtype( - xp: Any, dtype: DataType + xp: Any, api_version: NominalVersion, dtype: DataType ) -> Type[Union[bool, int, float, complex]]: """Returns builtin type which can have values that are castable to the given dtype, according to :xp-ref:`type promotion rules `. @@ -147,13 +147,17 @@ def find_castable_builtin_for_dtype( if dtype is not None and dtype in float_dtypes: return float - complex_dtypes, complex_stubs = partition_attributes_and_stubs(xp, COMPLEX_NAMES) - if dtype in complex_dtypes: - return complex - stubs.extend(int_stubs) stubs.extend(float_stubs) - stubs.extend(complex_stubs) + + if api_verson_gt(api_version, "2021.12"): + complex_dtypes, complex_stubs = partition_attributes_and_stubs( + xp, COMPLEX_NAMES + ) + if dtype in complex_dtypes: + return complex + stubs.extend(complex_stubs) + if len(stubs) > 0: warn_on_missing_dtypes(xp, stubs) raise InvalidArgument(f"dtype={dtype} not recognised in {xp.__name__}") @@ -177,6 +181,7 @@ def dtype_from_name(xp: Any, name: str) -> DataType: def _from_dtype( xp: Any, + api_version: NominalVersion, dtype: Union[DataType, str], *, min_value: Optional[Union[int, float]] = None, @@ -206,7 +211,7 @@ def _from_dtype( if isinstance(dtype, str): dtype = dtype_from_name(xp, dtype) - builtin = find_castable_builtin_for_dtype(xp, dtype) + builtin = find_castable_builtin_for_dtype(xp, api_version, dtype) def check_valid_minmax(prefix, val, info_obj): name = f"{prefix}_value" @@ -305,15 +310,15 @@ def check_valid_minmax(prefix, val, info_obj): "exclude_max": exclude_max, } if dtype == complex64: - floats = _from_dtype(xp, float32, **kw) + floats = _from_dtype(xp, api_version, float32, **kw) else: - floats = _from_dtype(xp, float64, **kw) + floats = _from_dtype(xp, api_version, float64, **kw) return st.builds(complex, floats, floats) class ArrayStrategy(st.SearchStrategy): - def __init__(self, xp, elements_strategy, dtype, shape, fill, unique): + def __init__(self, xp, api_version, elements_strategy, dtype, shape, fill, unique): self.xp = xp self.elements_strategy = elements_strategy self.dtype = dtype @@ -321,7 +326,7 @@ def __init__(self, xp, elements_strategy, dtype, shape, fill, unique): self.fill = fill self.unique = unique self.array_size = math.prod(shape) - self.builtin = find_castable_builtin_for_dtype(xp, dtype) + self.builtin = find_castable_builtin_for_dtype(xp, api_version, dtype) self.finfo = None if self.builtin is not float else xp.finfo(self.dtype) def check_set_value(self, val, val_0d, strategy): @@ -458,6 +463,7 @@ def do_draw(self, data): def _arrays( xp: Any, + api_version: NominalVersion, dtype: Union[DataType, str, st.SearchStrategy[DataType], st.SearchStrategy[str]], shape: Union[int, Shape, st.SearchStrategy[Shape]], *, @@ -540,14 +546,18 @@ def _arrays( if isinstance(dtype, st.SearchStrategy): return dtype.flatmap( - lambda d: _arrays(xp, d, shape, elements=elements, fill=fill, unique=unique) + lambda d: _arrays( + xp, api_version, d, shape, elements=elements, fill=fill, unique=unique + ) ) elif isinstance(dtype, str): dtype = dtype_from_name(xp, dtype) if isinstance(shape, st.SearchStrategy): return shape.flatmap( - lambda s: _arrays(xp, dtype, s, elements=elements, fill=fill, unique=unique) + lambda s: _arrays( + xp, api_version, dtype, s, elements=elements, fill=fill, unique=unique + ) ) elif isinstance(shape, int): shape = (shape,) @@ -559,9 +569,9 @@ def _arrays( ) if elements is None: - elements = _from_dtype(xp, dtype) + elements = _from_dtype(xp, api_version, dtype) elif isinstance(elements, Mapping): - elements = _from_dtype(xp, dtype, **elements) + elements = _from_dtype(xp, api_version, dtype, **elements) check_strategy(elements, "elements") if fill is None: @@ -572,7 +582,7 @@ def _arrays( fill = elements check_strategy(fill, "fill") - return ArrayStrategy(xp, elements, dtype, shape, fill, unique) + return ArrayStrategy(xp, api_version, elements, dtype, shape, fill, unique) @check_function @@ -912,6 +922,7 @@ def from_dtype( ) -> st.SearchStrategy[Union[bool, int, float]]: return _from_dtype( xp, + api_version, dtype, min_value=min_value, max_value=max_value, @@ -935,6 +946,7 @@ def arrays( ) -> st.SearchStrategy: return _arrays( xp, + api_version, dtype, shape, elements=elements, diff --git a/hypothesis-python/tests/array_api/test_from_dtype.py b/hypothesis-python/tests/array_api/test_from_dtype.py index f45cf6d852..18f6d081f1 100644 --- a/hypothesis-python/tests/array_api/test_from_dtype.py +++ b/hypothesis-python/tests/array_api/test_from_dtype.py @@ -36,7 +36,7 @@ def test_produces_castable_instances_from_dtype(xp, xps, dtype_name): """Strategies inferred by dtype generate values of a builtin type castable to the dtype.""" dtype = getattr(xp, dtype_name) - builtin = find_castable_builtin_for_dtype(xp, dtype) + builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype) assert_all_examples(xps.from_dtype(dtype), lambda v: isinstance(v, builtin)) @@ -45,7 +45,7 @@ def test_produces_castable_instances_from_name(xp, xps, dtype_name): """Strategies inferred by dtype name generate values of a builtin type castable to the dtype.""" dtype = getattr(xp, dtype_name) - builtin = find_castable_builtin_for_dtype(xp, dtype) + builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype) assert_all_examples(xps.from_dtype(dtype_name), lambda v: isinstance(v, builtin)) From c9018d061d5b759fb9b5ccf81e3dc25251fb4a8f Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 11:33:12 +0100 Subject: [PATCH 17/62] Spelling fix for `api_vers{i}on_gt()` --- hypothesis-python/src/hypothesis/extra/array_api.py | 10 +++++----- hypothesis-python/tests/array_api/conftest.py | 4 ++-- .../tests/array_api/test_scalar_dtypes.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 23970b6520..ad680c1a10 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -63,13 +63,13 @@ ] -# Be sure to keep versions in ascending order so api_verson_gt() works +# Be sure to keep versions in ascending order so api_version_gt() works RELEASED_VERSIONS = ("2021.12",) NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) NominalVersion = Literal[NOMINAL_VERSIONS] -def api_verson_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: +def api_version_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: return NOMINAL_VERSIONS.index(api_version1) > NOMINAL_VERSIONS.index(api_version2) @@ -150,7 +150,7 @@ def find_castable_builtin_for_dtype( stubs.extend(int_stubs) stubs.extend(float_stubs) - if api_verson_gt(api_version, "2021.12"): + if api_version_gt(api_version, "2021.12"): complex_dtypes, complex_stubs = partition_attributes_and_stubs( xp, COMPLEX_NAMES ) @@ -627,7 +627,7 @@ def _numeric_dtypes( ) -> st.SearchStrategy[DataType]: """Return a strategy for all numeric dtype objects.""" strat = _real_dtypes(xp) - if api_verson_gt(api_version, "2021.12"): + if api_version_gt(api_version, "2021.12"): strat |= _complex_dtypes(xp) return strat @@ -1020,7 +1020,7 @@ def __repr__(self): indices=indices, ) - if api_verson_gt(api_version, "2021.12"): + if api_version_gt(api_version, "2021.12"): @defines_strategy() def complex_dtypes( diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 08b64ecc78..eeefa0af7e 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -18,7 +18,7 @@ from hypothesis.errors import HypothesisWarning from hypothesis.extra.array_api import ( RELEASED_VERSIONS, - api_verson_gt, + api_version_gt, make_strategies_namespace, mock_xp, ) @@ -96,7 +96,7 @@ def pytest_collection_modifyitems(config, items): else: item.callspec.params["xps"].api_version min_version: Literal[RELEASED_VERSIONS] = marker.args[0] - if api_verson_gt(min_version, item.callspec.params["xps"].api_version): + if api_version_gt(min_version, item.callspec.params["xps"].api_version): item.add_marker( pytest.mark.skip(reason=f"requires api_version=>{min_version}") ) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index e9c2b3cc3d..e444782f5b 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -18,14 +18,14 @@ NUMERIC_NAMES, REAL_NAMES, UINT_NAMES, - api_verson_gt, + api_version_gt, ) from tests.common.debug import assert_all_examples, find_any, minimal def test_can_generate_scalar_dtypes(xp, xps): - if api_verson_gt(xps.api_version, "2021.12"): + if api_version_gt(xps.api_version, "2021.12"): dtypes = [getattr(xp, name) for name in DTYPE_NAMES] else: dtypes = [getattr(xp, name) for name in ("bool",) + REAL_NAMES] @@ -37,7 +37,7 @@ def test_can_generate_boolean_dtypes(xp, xps): def test_can_generate_numeric_dtypes(xp, xps): - if api_verson_gt(xps.api_version, "2021.12"): + if api_version_gt(xps.api_version, "2021.12"): numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES] else: numeric_dtypes = [getattr(xp, name) for name in REAL_NAMES] From 21893740ccde154472549307b014aa634c1625d7 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 11:39:38 +0100 Subject: [PATCH 18/62] Change `test_draw_arrays_from_dtype_name_strategies` on runtime --- .../tests/array_api/test_arrays.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 2c6fda3421..1b926a2077 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -12,7 +12,7 @@ from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import DTYPE_NAMES +from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES, api_version_gt from hypothesis.internal.floats import width_smallest_normals from tests.array_api.common import dtype_name_params, flushes_to_zero @@ -88,14 +88,16 @@ def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name): find_any(xps.arrays(strat, ())) -@given( - strat=st.lists(st.sampled_from(DTYPE_NAMES), min_size=1, unique=True).flatmap( - st.sampled_from - ) -) -def test_draw_arrays_from_dtype_name_strategies(xp, xps, strat): +@given(data=st.data()) +def test_draw_arrays_from_dtype_name_strategies(xp, xps, data): """Draw arrays from dtype name strategies.""" - find_any(xps.arrays(strat, ())) + all_names = ("bool",) + REAL_NAMES + if api_version_gt(xps.api_version, "2021.12"): + all_names += COMPLEX_NAMES + sample_names = data.draw( + st.lists(st.sampled_from(all_names), min_size=1, unique=True) + ) + find_any(xps.arrays(st.sampled_from(sample_names), ())) def test_generate_arrays_from_shapes_strategy(xp, xps): From 4106eb8408736efa09a187a7027835c37350b743 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 12:52:20 +0100 Subject: [PATCH 19/62] Fix not-actually-literally `Literal` uses --- hypothesis-python/src/hypothesis/extra/array_api.py | 4 +++- hypothesis-python/tests/array_api/conftest.py | 5 ++--- .../tests/array_api/test_partial_adoptors.py | 10 ++++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index ad680c1a10..77cfcaf9a7 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -65,8 +65,10 @@ # Be sure to keep versions in ascending order so api_version_gt() works RELEASED_VERSIONS = ("2021.12",) +assert sorted(RELEASED_VERSIONS) == list(RELEASED_VERSIONS) # sanity check NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) -NominalVersion = Literal[NOMINAL_VERSIONS] +NominalVersion = Literal["2021.12", "draft"] +assert NominalVersion.__args__ == NOMINAL_VERSIONS # sanity check def api_version_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index eeefa0af7e..d6dab4c9cb 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -11,13 +11,12 @@ import warnings from importlib import import_module from os import getenv -from typing import Literal import pytest from hypothesis.errors import HypothesisWarning from hypothesis.extra.array_api import ( - RELEASED_VERSIONS, + NominalVersion, api_version_gt, make_strategies_namespace, mock_xp, @@ -95,7 +94,7 @@ def pytest_collection_modifyitems(config, items): pass else: item.callspec.params["xps"].api_version - min_version: Literal[RELEASED_VERSIONS] = marker.args[0] + min_version: NominalVersion = marker.args[0] if api_version_gt(min_version, item.callspec.params["xps"].api_version): item.add_marker( pytest.mark.skip(reason=f"requires api_version=>{min_version}") diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index 350af87111..adf2c568a2 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -11,7 +11,7 @@ from copy import copy from functools import lru_cache from types import SimpleNamespace -from typing import List, Literal, Optional, Tuple +from typing import List, Optional, Tuple import pytest @@ -24,6 +24,7 @@ INT_NAMES, RELEASED_VERSIONS, UINT_NAMES, + NominalVersion, make_strategies_namespace, mock_xp, ) @@ -125,10 +126,11 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data): class MockArray: - def __init__(self, supported_versions: Tuple[Literal[RELEASED_VERSIONS], ...]): + def __init__(self, supported_versions: Tuple[NominalVersion, ...]): + assert len(set(supported_versions)) == len(supported_versions) # sanity check self.supported_versions = supported_versions - def __array_namespace__(self, *, api_version: Optional[str] = None): + def __array_namespace__(self, *, api_version: Optional[NominalVersion] = None): if api_version is not None and api_version not in self.supported_versions: raise return SimpleNamespace( @@ -136,7 +138,7 @@ def __array_namespace__(self, *, api_version: Optional[str] = None): ) -version_permutations: List[Tuple[Literal[RELEASED_VERSIONS], ...]] = [ +version_permutations: List[Tuple[NominalVersion, ...]] = [ RELEASED_VERSIONS[:i] for i in range(1, len(RELEASED_VERSIONS) + 1) ] From cf2d96a85584d814bfa733cd9009595ed71c66cd Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 13:07:06 +0100 Subject: [PATCH 20/62] Remove use of value-bound kwargs for `xps.from_dtype()` on complex --- .../src/hypothesis/extra/array_api.py | 4 ---- .../array_api/test_argument_validation.py | 23 ------------------- 2 files changed, 27 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 77cfcaf9a7..44e885e1c0 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -303,13 +303,9 @@ def check_valid_minmax(prefix, val, info_obj): raise NotImplementedError() from e # TODO else: kw = { - "min_value": min_value, - "max_value": max_value, "allow_nan": allow_nan, "allow_infinity": allow_infinity, "allow_subnormal": allow_subnormal, - "exclude_min": exclude_min, - "exclude_max": exclude_max, } if dtype == complex64: floats = _from_dtype(xp, api_version, float32, **kw) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 746dc43854..b22ec89006 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -53,27 +53,8 @@ def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): e("from_dtype", dtype="int8", max_value="not an int"), e("from_dtype", dtype="float32", min_value="not a float"), e("from_dtype", dtype="float32", max_value="not a float"), - e( - "from_dtype", - _min_version="draft", - dtype="complex64", - min_value="not a float", - ), - e( - "from_dtype", - _min_version="draft", - dtype="complex64", - max_value="not a float", - ), e("from_dtype", dtype="int8", min_value=10, max_value=5), e("from_dtype", dtype="float32", min_value=10, max_value=5), - e( - "from_dtype", - _min_version="draft", - dtype="complex64", - min_value=10, - max_value=5, - ), e("from_dtype", dtype="int8", min_value=-999), e("from_dtype", dtype="int8", max_value=-999), e("from_dtype", dtype="int8", min_value=999), @@ -86,10 +67,6 @@ def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): e("from_dtype", dtype="float32", max_value=-4e38), e("from_dtype", dtype="float32", min_value=4e38), e("from_dtype", dtype="float32", max_value=4e38), - e("from_dtype", _min_version="draft", dtype="complex64", min_value=-4e38), - e("from_dtype", _min_version="draft", dtype="complex64", max_value=-4e38), - e("from_dtype", _min_version="draft", dtype="complex64", min_value=4e38), - e("from_dtype", _min_version="draft", dtype="complex64", max_value=4e38), e("integer_dtypes", sizes=()), e("integer_dtypes", sizes=(3,)), e("unsigned_integer_dtypes", sizes=()), From b35de4507033a297a9ade5b73c01cfb668294604 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 13:14:52 +0100 Subject: [PATCH 21/62] Improve error message for invalid `api_version` --- hypothesis-python/src/hypothesis/extra/array_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 44e885e1c0..c475354aa3 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -865,7 +865,9 @@ def make_strategies_namespace( api_version == Ellipsis or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), f"{api_version=}, but api_version must be an ellipsis (...), or valid " - f"version string {RELEASED_VERSIONS}", + f"version string {RELEASED_VERSIONS}. If the standard version you want " + "is not available, please ensure you're using the latest version of " + "Hypothesis, then open an issue if one doesn't already exist.", ) if api_version == Ellipsis: # When api_version=..., we infer the most recent API version for which From dcb426e0393cdaf671b58bf62283ef863c6dd7e8 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 16 Sep 2022 13:24:09 +0100 Subject: [PATCH 22/62] Parametrize `xp` for `test_make_namespace_raises_invalid_argument` --- .../tests/array_api/test_argument_validation.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index b22ec89006..8055257de1 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -13,11 +13,7 @@ import pytest from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import ( - NominalVersion, - make_strategies_namespace, - mock_xp, -) +from hypothesis.extra.array_api import NominalVersion, make_strategies_namespace def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): @@ -231,6 +227,6 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs): @pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) -def test_make_namespace_raise_invalid_argument(api_version): +def test_make_namespace_raise_invalid_argument(xp, xps, api_version): with pytest.raises(InvalidArgument): - make_strategies_namespace(mock_xp, api_version=api_version) + make_strategies_namespace(xp, api_version=api_version) From bf020f4c6e4058063fd83d9032a1d44f2bc7b6e2 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 08:55:06 +0100 Subject: [PATCH 23/62] Cache `make_strategies_namespace()` with a `WeakValueDictionary()` --- .../src/hypothesis/extra/array_api.py | 22 ++++++++++-- .../tests/array_api/test_caching.py | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 hypothesis-python/tests/array_api/test_caching.py diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index c475354aa3..e67336655c 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -28,6 +28,7 @@ Union, ) from warnings import warn +from weakref import WeakValueDictionary from hypothesis import strategies as st from hypothesis.errors import HypothesisWarning, InvalidArgument @@ -836,6 +837,10 @@ def indices( ) +# Cache for make_strategies_namespace() +_args_to_xps = WeakValueDictionary() + + def make_strategies_namespace( xp: Any, *, api_version: Union["Ellipsis", NominalVersion] = ... ) -> SimpleNamespace: @@ -861,6 +866,13 @@ def make_strategies_namespace( True """ + try: + namespace = _args_to_xps[(xp, api_version)] + except (KeyError, TypeError): + pass + else: + return namespace + check_argument( api_version == Ellipsis or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), @@ -1031,7 +1043,13 @@ def complex_dtypes( complex_dtypes.__doc__ = _complex_dtypes.__doc__ kwargs["complex_dtypes"] = complex_dtypes - return PrettySimpleNamespace(**kwargs) + namespace = PrettySimpleNamespace(**kwargs) + try: + _args_to_xps[(xp, api_version)] = namespace + except TypeError: + pass + + return namespace try: @@ -1074,7 +1092,7 @@ def mock_finfo(dtype: DataType) -> FloatInfo: ) mock_xp = SimpleNamespace( - __name__="mockpy", + __name__="mock", # Data types int8=np.int8, int16=np.int16, diff --git a/hypothesis-python/tests/array_api/test_caching.py b/hypothesis-python/tests/array_api/test_caching.py new file mode 100644 index 0000000000..70f3bbb954 --- /dev/null +++ b/hypothesis-python/tests/array_api/test_caching.py @@ -0,0 +1,36 @@ +# This file is part of Hypothesis, which may be found at +# https://github.com/HypothesisWorks/hypothesis/ +# +# Copyright the Hypothesis Authors. +# Individual contributors are listed in AUTHORS.rst and the git log. +# +# 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/. + +from types import SimpleNamespace +from weakref import WeakValueDictionary + +import pytest + +from hypothesis.extra import array_api + + +@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") +def test_make_strategies_namespace(xp, xps, monkeypatch): + try: + hash(xp) + except TypeError: + pytest.skip("xp not hashable") + assert isinstance(array_api._args_to_xps, WeakValueDictionary) + monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary()) + assert len(array_api._args_to_xps) == 0 # sanity check + xps1 = array_api.make_strategies_namespace(xp, api_version="2021.12") + assert len(array_api._args_to_xps) == 1 + xps2 = array_api.make_strategies_namespace(xp, api_version="2021.12") + assert len(array_api._args_to_xps) == 1 + assert isinstance(xps2, SimpleNamespace) + assert xps2 is xps1 + del xps1 + del xps2 + assert len(array_api._args_to_xps) == 0 From 5cad500b3f1a37756b04abc43cf7aee32a51783f Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 09:10:50 +0100 Subject: [PATCH 24/62] `test/array_api/conftest.py` changes * Mock always tested with `HYPOTHESIS_TEST_ARRAY_API=default` * Param ids generated dynamically in the parametrize stage * Tests just using `xp` get parametrized too --- hypothesis-python/tests/array_api/README.md | 2 +- hypothesis-python/tests/array_api/conftest.py | 30 +++++++++++++------ .../array_api/test_argument_validation.py | 2 +- .../tests/array_api/test_caching.py | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md index e3dcefa03f..ea0b12bb54 100644 --- a/hypothesis-python/tests/array_api/README.md +++ b/hypothesis-python/tests/array_api/README.md @@ -10,7 +10,7 @@ You can test other array modules which adopt the Array API via the `HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized options: -* `"default"`: only uses `numpy.array_api`, or if not available, fallbacks to the mock. +* `"default"`: uses the mock, and `numpy.array_api` if available. * `"all"`: uses all array modules found via entry points, _and_ the mock. If neither of these, the test suite will then try resolve the variable like so: diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index d6dab4c9cb..1d35948305 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -38,32 +38,34 @@ # Specifically `params` is a list of pytest parameters, with each parameter # containing the array module and its respective strategies namespace. if test_xp_option == "default": + xp_and_xps_pairs = [(mock_xp, mock_xps)] try: xp = name_to_entry_point["numpy"].load() - xps = make_strategies_namespace(xp) - params = [pytest.param(xp, xps, id=f"numpy-{xps.api_version}")] except KeyError: - params = [pytest.param(mock_xp, mock_xps, id="mock")] + pass + else: + xps = make_strategies_namespace(xp) + xp_and_xps_pairs.append((xp, xps)) elif test_xp_option == "all": if len(name_to_entry_point) == 0: raise ValueError( "HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found" ) - params = [pytest.param(mock_xp, mock_xps, id="mock-draft")] + xp_and_xps_pairs = [(mock_xp, mock_xps)] for name, ep in name_to_entry_point.items(): xp = ep.load() xps = make_strategies_namespace(xp) - params.append(pytest.param(xp, xps, id=f"{name}-{xps.api_version}")) + xp_and_xps_pairs.append((xp, xps)) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() xps = make_strategies_namespace(xp) - params = [pytest.param(xp, xps, id=f"{test_xp_option}-{xps.api_version}")] + xp_and_xps_pairs = [(xp, xps)] else: try: xp = import_module(test_xp_option) xps = make_strategies_namespace(xp) - params = [pytest.param(xp, xps, id=f"{test_xp_option}-{xps.api_version}")] + xp_and_xps_pairs = [(xp, xps)] except ImportError as e: raise ValueError( f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid " @@ -73,8 +75,18 @@ def pytest_generate_tests(metafunc): - if "xp" in metafunc.fixturenames and "xps" in metafunc.fixturenames: - metafunc.parametrize("xp, xps", params) + xp_params = [] + xp_and_xps_params = [] + for xp, xps in xp_and_xps_pairs: + xp_params.append(pytest.param(xp, id=xp.__name__)) + xp_and_xps_params.append( + pytest.param(xp, xps, id=f"{xp.__name__}-{xps.api_version}") + ) + if "xp" in metafunc.fixturenames: + if "xps" in metafunc.fixturenames: + metafunc.parametrize("xp, xps", xp_and_xps_params) + else: + metafunc.parametrize("xp", xp_params) def pytest_configure(config): diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 8055257de1..fb1a2688bb 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -227,6 +227,6 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs): @pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) -def test_make_namespace_raise_invalid_argument(xp, xps, api_version): +def test_make_namespace_raise_invalid_argument(xp, api_version): with pytest.raises(InvalidArgument): make_strategies_namespace(xp, api_version=api_version) diff --git a/hypothesis-python/tests/array_api/test_caching.py b/hypothesis-python/tests/array_api/test_caching.py index 70f3bbb954..7c209925fc 100644 --- a/hypothesis-python/tests/array_api/test_caching.py +++ b/hypothesis-python/tests/array_api/test_caching.py @@ -17,7 +17,7 @@ @pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") -def test_make_strategies_namespace(xp, xps, monkeypatch): +def test_make_strategies_namespace(xp, monkeypatch): try: hash(xp) except TypeError: From 7af2d8d5fb3171f1871340b46d3f6a8953b09c89 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 11:51:15 +0100 Subject: [PATCH 25/62] Env for specifying `api_version` in `tests/array_api/ --- hypothesis-python/tests/array_api/README.md | 23 ++++++++++--- hypothesis-python/tests/array_api/conftest.py | 34 +++++++++++++------ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md index ea0b12bb54..093cfe35ef 100644 --- a/hypothesis-python/tests/array_api/README.md +++ b/hypothesis-python/tests/array_api/README.md @@ -1,10 +1,13 @@ This folder contains tests for `hypothesis.extra.array_api`. -## Running against different array modules +## Mocked array module + +A mock of the Array API namespace exists as `mock_xp` in `extra.array_api`. This +wraps NumPy-proper to conform it to the *draft* spec, where `numpy.array_api` +might not. This is not a fully compliant wrapper, but conforms enough for the +purposes of testing. -By default it will run against `numpy.array_api`. If that's not available -(likely because an older NumPy version is installed), these tests will fallback -to using the mock defined at the bottom of `src/hypothesis/extra/array_api.py`. +## Running against different array modules You can test other array modules which adopt the Array API via the `HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized @@ -30,3 +33,15 @@ or use the import path (**2.**), The former method is more ergonomic, but as entry points are optional for adopting the Array API, you will need to use the latter method for libraries that opt-out. + + +## Running against different API versions + +You can specify the `api_version` to use when testing array modules via the +`HYPOTHESIS_TEST_ARRAY_API_VERSION` environment variable. There is one +recognized option: + +* `"default"`: infers the latest API version for each array module. + +Otherwise the test suite will use the variable as the `api_version` argument for +`make_strategies_namespace()`. diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 1d35948305..5e9f345a6d 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -16,6 +16,7 @@ from hypothesis.errors import HypothesisWarning from hypothesis.extra.array_api import ( + NOMINAL_VERSIONS, NominalVersion, api_version_gt, make_strategies_namespace, @@ -24,19 +25,30 @@ from tests.array_api.common import installed_array_modules +# See README.md in regards to the env variables +test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default") +test_version_option = getenv("HYPOTHESIS_TEST_ARRAY_API_VERSION", "default") +if test_version_option != "default" and test_version_option not in NOMINAL_VERSIONS: + raise ValueError( + f"HYPOTHESIS_TEST_ARRAY_API_VERSION='{test_version_option}' is not " + f"'default' or a valid api_version {NOMINAL_VERSIONS}." + ) with pytest.warns(HypothesisWarning): - mock_xps = make_strategies_namespace(mock_xp, api_version="draft") + mock_xps = make_strategies_namespace( + mock_xp, + api_version="draft" + if test_version_option == "default" + else test_version_option, + ) +api_version = ... if test_version_option == "default" else test_version_option -# See README.md in regards to the HYPOTHESIS_TEST_ARRAY_API env variable -test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default") name_to_entry_point = installed_array_modules() with warnings.catch_warnings(): # We ignore all warnings here as many array modules warn on import warnings.simplefilter("ignore") - # We go through the steps described in README.md to define `params`, which - # contains the array module(s) to be ran against the test suite. - # Specifically `params` is a list of pytest parameters, with each parameter - # containing the array module and its respective strategies namespace. + # We go through the steps described in README.md to define `xp_xps_parais`, + # which contains the array module(s) to be ran against the test suite along + # with their respective strategy namespaces. if test_xp_option == "default": xp_and_xps_pairs = [(mock_xp, mock_xps)] try: @@ -44,7 +56,7 @@ except KeyError: pass else: - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs.append((xp, xps)) elif test_xp_option == "all": if len(name_to_entry_point) == 0: @@ -54,17 +66,17 @@ xp_and_xps_pairs = [(mock_xp, mock_xps)] for name, ep in name_to_entry_point.items(): xp = ep.load() - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs.append((xp, xps)) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] else: try: xp = import_module(test_xp_option) - xps = make_strategies_namespace(xp) + xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] except ImportError as e: raise ValueError( From df88bf4d813f7e65cd6a9b3ba5ad85e9c74175f0 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 12:29:19 +0100 Subject: [PATCH 26/62] Hold meta attributes in strategies namespace and validate for them --- .../src/hypothesis/extra/array_api.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index e67336655c..7f7f9f280d 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -1010,11 +1010,21 @@ def floating_dtypes( unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__ floating_dtypes.__doc__ = _floating_dtypes.__doc__ - class PrettySimpleNamespace(SimpleNamespace): + class StrategiesNamespace(SimpleNamespace): + def __init__(self, **kwargs): + for attr in ["name", "api_version"]: + if attr not in kwargs.keys(): + raise ValueError(f"'{attr}' kwarg required") + super().__init__(**kwargs) + def __repr__(self): - return f"make_strategies_namespace({xp.__name__}, {api_version=})" + return ( + f"make_strategies_namespace(" + f"{self.name}, api_version='{self.api_version}')" + ) kwargs = dict( + name=xp.__name__, api_version=api_version, from_dtype=from_dtype, arrays=arrays, @@ -1043,7 +1053,7 @@ def complex_dtypes( complex_dtypes.__doc__ = _complex_dtypes.__doc__ kwargs["complex_dtypes"] = complex_dtypes - namespace = PrettySimpleNamespace(**kwargs) + namespace = StrategiesNamespace(**kwargs) try: _args_to_xps[(xp, api_version)] = namespace except TypeError: From cbd157f0abea08611ba17d667b6ebc942ac3c9ad Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 12:59:28 +0100 Subject: [PATCH 27/62] `xps.complex_dtypes` raises when `api_version="2021.12"` --- .../src/hypothesis/extra/array_api.py | 10 ++++++++++ ...aching.py => test_strategies_namespace.py} | 20 +++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) rename hypothesis-python/tests/array_api/{test_caching.py => test_strategies_namespace.py} (64%) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 7f7f9f280d..6822d58447 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -1017,6 +1017,16 @@ def __init__(self, **kwargs): raise ValueError(f"'{attr}' kwarg required") super().__init__(**kwargs) + @property + def complex_dtypes(self): + try: + return self.__dict__["complex_dtypes"] + except KeyError: + raise AttributeError( + "You attempted to access 'complex_dtypes', but it is not " + f"available for api_version='{self.api_version}'." + ) + def __repr__(self): return ( f"make_strategies_namespace(" diff --git a/hypothesis-python/tests/array_api/test_caching.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py similarity index 64% rename from hypothesis-python/tests/array_api/test_caching.py rename to hypothesis-python/tests/array_api/test_strategies_namespace.py index 7c209925fc..ce0671f441 100644 --- a/hypothesis-python/tests/array_api/test_caching.py +++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py @@ -14,10 +14,17 @@ import pytest from hypothesis.extra import array_api +from hypothesis.extra.array_api import ( + NOMINAL_VERSIONS, + make_strategies_namespace, + mock_xp, +) +from hypothesis.strategies import SearchStrategy +pytestmark = pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") -@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") -def test_make_strategies_namespace(xp, monkeypatch): + +def test_caching(xp, monkeypatch): try: hash(xp) except TypeError: @@ -34,3 +41,12 @@ def test_make_strategies_namespace(xp, monkeypatch): del xps1 del xps2 assert len(array_api._args_to_xps) == 0 + + +def test_complex_dtypes_raises_on_first_version(): + first_xps = make_strategies_namespace(mock_xp, api_version=NOMINAL_VERSIONS[0]) + with pytest.raises(AttributeError): + first_xps.complex_dtypes() + for api_version in NOMINAL_VERSIONS[1:]: + xps = make_strategies_namespace(mock_xp, api_version=api_version) + assert isinstance(xps.complex_dtypes(), SearchStrategy) From 47a823f7fc253fd2b2ec65f54f172c9834aa5a54 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 13:23:06 +0100 Subject: [PATCH 28/62] Descriptive comment and error for complex branch in `xps.from_dtype()` --- .../src/hypothesis/extra/array_api.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 6822d58447..532888e75e 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -296,24 +296,29 @@ def check_valid_minmax(prefix, val, info_obj): return st.floats(width=finfo.bits, **kw) else: - try: - float32 = xp.float32 - float64 = xp.float64 - complex64 = xp.complex64 - except AttributeError: - raise NotImplementedError() from e # TODO + # A less-inelegant solution to support complex dtypes exists, but as + # this is currently a draft feature, we might as well wait for + # discussion of complex inspection to resolve first - a better method + # might become available soon enough. + # See https://github.com/data-apis/array-api/issues/433 + for attr in ["float32", "float64", "complex64"]: + if not hasattr(xp, attr): + raise NotImplementedError( + f"Array module {xp.__name__} has no dtype {attr}, which is " + "currently required for xps.from_dtype() to work with " + "any complex dtype." + ) + kw = { + "allow_nan": allow_nan, + "allow_infinity": allow_infinity, + "allow_subnormal": allow_subnormal, + } + if dtype == xp.complex64: + floats = _from_dtype(xp, api_version, xp.float32, **kw) else: - kw = { - "allow_nan": allow_nan, - "allow_infinity": allow_infinity, - "allow_subnormal": allow_subnormal, - } - if dtype == complex64: - floats = _from_dtype(xp, api_version, float32, **kw) - else: - floats = _from_dtype(xp, api_version, float64, **kw) + floats = _from_dtype(xp, api_version, xp.float64, **kw) - return st.builds(complex, floats, floats) + return st.builds(complex, floats, floats) class ArrayStrategy(st.SearchStrategy): From a19b8dc581d079f898b153a9007fedc0cbebb954 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 19 Sep 2022 13:42:06 +0100 Subject: [PATCH 29/62] Prettier `mock_xps` definition --- hypothesis-python/tests/array_api/conftest.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 5e9f345a6d..8cf82f086c 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -34,12 +34,8 @@ f"'default' or a valid api_version {NOMINAL_VERSIONS}." ) with pytest.warns(HypothesisWarning): - mock_xps = make_strategies_namespace( - mock_xp, - api_version="draft" - if test_version_option == "default" - else test_version_option, - ) + 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 = ... if test_version_option == "default" else test_version_option name_to_entry_point = installed_array_modules() From 2f8042e2462062f287475c1b5b98157fbc3e4a85 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 09:36:40 +0100 Subject: [PATCH 30/62] Minor doc fixes --- hypothesis-python/src/hypothesis/extra/array_api.py | 2 +- hypothesis-python/tests/array_api/conftest.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 532888e75e..c8ccea0527 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -618,7 +618,7 @@ def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]: def _real_dtypes(xp: Any) -> st.SearchStrategy[DataType]: - """Return a strategy for all real dtype objects.""" + """Return a strategy for all real-valued dtype objects.""" return st.one_of( _integer_dtypes(xp), _unsigned_integer_dtypes(xp), diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 8cf82f086c..d70d264d86 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -42,8 +42,8 @@ with warnings.catch_warnings(): # We ignore all warnings here as many array modules warn on import warnings.simplefilter("ignore") - # We go through the steps described in README.md to define `xp_xps_parais`, - # which contains the array module(s) to be ran against the test suite along + # We go through the steps described in README.md to define `xp_xps_pairs`, + # which contains the array module(s) to be run against the test suite, along # with their respective strategy namespaces. if test_xp_option == "default": xp_and_xps_pairs = [(mock_xp, mock_xps)] From bcfe278c2be415cea85b30849c4aa7c2e4b5e534 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 09:40:54 +0100 Subject: [PATCH 31/62] Use kw-only args for `array_api.ArrayStrategy` --- .../src/hypothesis/extra/array_api.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index c8ccea0527..06424dbbaf 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -322,7 +322,9 @@ def check_valid_minmax(prefix, val, info_obj): class ArrayStrategy(st.SearchStrategy): - def __init__(self, xp, api_version, elements_strategy, dtype, shape, fill, unique): + def __init__( + self, *, xp, api_version, elements_strategy, dtype, shape, fill, unique + ): self.xp = xp self.elements_strategy = elements_strategy self.dtype = dtype @@ -586,7 +588,15 @@ def _arrays( fill = elements check_strategy(fill, "fill") - return ArrayStrategy(xp, api_version, elements, dtype, shape, fill, unique) + return ArrayStrategy( + xp=xp, + api_version=api_version, + elements_strategy=elements, + dtype=dtype, + shape=shape, + fill=fill, + unique=unique, + ) @check_function From 3156ccb44de42a98af565b4b33f17a492c3e211b Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 09:44:31 +0100 Subject: [PATCH 32/62] Use `contextlib.suppress()` for `api_version` inferrence --- hypothesis-python/src/hypothesis/extra/array_api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 06424dbbaf..94546c8616 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -8,6 +8,7 @@ # 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 contextlib import math import sys from numbers import Real @@ -918,11 +919,8 @@ def make_strategies_namespace( except Exception: raise InvalidArgument(errmsg) for api_version in reversed(RELEASED_VERSIONS): - try: + with contextlib.suppress(Exception): xp = array.__array_namespace__(api_version=api_version) - except Exception: - pass - else: break # i.e. a valid xp and api_version has been inferred else: raise InvalidArgument(errmsg) From b3ce0e2a49a0cfda1e9aa5ec54360d22165d72c1 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 09:58:18 +0100 Subject: [PATCH 33/62] `None` replaces `...` for `api_version` --- .../src/hypothesis/extra/array_api.py | 14 +++++++------- hypothesis-python/tests/array_api/conftest.py | 2 +- .../tests/array_api/test_argument_validation.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 94546c8616..d0f84d2c47 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -858,7 +858,7 @@ def indices( def make_strategies_namespace( - xp: Any, *, api_version: Union["Ellipsis", NominalVersion] = ... + xp: Any, *, api_version: Optional[NominalVersion] = None ) -> SimpleNamespace: """Creates a strategies namespace for the given array module. @@ -890,15 +890,15 @@ def make_strategies_namespace( return namespace check_argument( - api_version == Ellipsis + api_version is None or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), - f"{api_version=}, but api_version must be an ellipsis (...), or valid " - f"version string {RELEASED_VERSIONS}. If the standard version you want " - "is not available, please ensure you're using the latest version of " + f"{api_version=}, but api_version must be None, or a valid version " + f"string {RELEASED_VERSIONS}. If the standard version you want is not " + "available, please ensure you're using the latest version of " "Hypothesis, then open an issue if one doesn't already exist.", ) - if api_version == Ellipsis: - # When api_version=..., we infer the most recent API version for which + if api_version is None: + # When api_version=None, we infer the most recent API version for which # the passed xp is valid. We go through the released versions in # descending order, passing them to x.__array_namespace__() until no # errors are raised, thus inferring that specific api_version is diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index d70d264d86..d80c4fd51d 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -36,7 +36,7 @@ with pytest.warns(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 = ... if test_version_option == "default" else test_version_option +api_version = None if test_version_option == "default" else test_version_option name_to_entry_point = installed_array_modules() with warnings.catch_warnings(): diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index fb1a2688bb..433f9a55c9 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -226,7 +226,7 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs): strat.example() -@pytest.mark.parametrize("api_version", [None, "latest", "1970.01", 42]) +@pytest.mark.parametrize("api_version", [..., "latest", "1970.01", 42]) def test_make_namespace_raise_invalid_argument(xp, api_version): with pytest.raises(InvalidArgument): make_strategies_namespace(xp, api_version=api_version) From 4bd1dfa554be637048ba5d162652012ba62ff16b Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 10:12:02 +0100 Subject: [PATCH 34/62] Note on future `api_version` test parametrization in readme --- hypothesis-python/tests/array_api/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md index 093cfe35ef..634cf738be 100644 --- a/hypothesis-python/tests/array_api/README.md +++ b/hypothesis-python/tests/array_api/README.md @@ -45,3 +45,7 @@ recognized option: Otherwise the test suite will use the variable as the `api_version` argument for `make_strategies_namespace()`. + +In the future we intend to support running tests against multiple API versioned +namespaces, likely with an additional recognized option that infers all +supported versions. From 68593b5cd70b68f59462333a7719095b151dd15b Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 10:28:00 +0100 Subject: [PATCH 35/62] `MIN_VER_FOR_COMPLEX` constant for test suite --- hypothesis-python/tests/array_api/common.py | 13 +++++++++---- .../tests/array_api/test_argument_validation.py | 6 ++++-- hypothesis-python/tests/array_api/test_arrays.py | 12 +++++++++--- hypothesis-python/tests/array_api/test_pretty.py | 10 ++++++++-- .../tests/array_api/test_scalar_dtypes.py | 3 ++- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py index 4e60ad35ce..a816a62cb0 100644 --- a/hypothesis-python/tests/array_api/common.py +++ b/hypothesis-python/tests/array_api/common.py @@ -13,16 +13,21 @@ import pytest -from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES +from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES, NominalVersion from hypothesis.internal.floats import next_up __all__ = [ + "MIN_VER_FOR_COMPLEX:", "installed_array_modules", "flushes_to_zero", "dtype_name_params", ] +# This should be updated for when a released version includes complex numbers +MIN_VER_FOR_COMPLEX: NominalVersion = "draft" + + def installed_array_modules() -> Dict[str, EntryPoint]: """Returns a dictionary of array module names paired to their entry points @@ -55,6 +60,6 @@ def flushes_to_zero(xp, width: int) -> bool: dtype_name_params = ["bool"] + list(REAL_NAMES) -dtype_name_params += [ - pytest.param(n, marks=pytest.mark.xp_min_version("draft")) for n in COMPLEX_NAMES -] +for name in COMPLEX_NAMES: + param = pytest.param(name, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)) + dtype_name_params.append(param) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 433f9a55c9..ad73c49665 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -15,6 +15,8 @@ from hypothesis.errors import InvalidArgument from hypothesis.extra.array_api import NominalVersion, make_strategies_namespace +from tests.array_api.common import MIN_VER_FOR_COMPLEX + def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items()) @@ -69,8 +71,8 @@ def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs): e("unsigned_integer_dtypes", sizes=(3,)), e("floating_dtypes", sizes=()), e("floating_dtypes", sizes=(3,)), - e("complex_dtypes", _min_version="draft", sizes=()), - e("complex_dtypes", _min_version="draft", sizes=(3,)), + e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=()), + e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=(3,)), e("valid_tuple_axes", ndim=-1), e("valid_tuple_axes", ndim=2, min_size=-1), e("valid_tuple_axes", ndim=2, min_size=3, max_size=10), diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 1b926a2077..78eb33ea02 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -15,7 +15,11 @@ from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES, api_version_gt from hypothesis.internal.floats import width_smallest_normals -from tests.array_api.common import dtype_name_params, flushes_to_zero +from tests.array_api.common import ( + MIN_VER_FOR_COMPLEX, + dtype_name_params, + flushes_to_zero, +) from tests.common.debug import assert_all_examples, find_any, minimal from tests.common.utils import flaky @@ -78,7 +82,9 @@ def test_draw_arrays_from_int_shapes(xp, xps, data): "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", - pytest.param("complex_dtypes", marks=pytest.mark.xp_min_version("draft")), + pytest.param( + "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) + ), ], ) def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name): @@ -303,7 +309,7 @@ def test_may_not_use_overflowing_integers(xp, xps, kwargs): pytest.param( "complex64", st.complex_numbers(min_magnitude=10**300, allow_infinity=False), - marks=pytest.mark.xp_min_version("draft"), + marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX), ), ], ) diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py index 0b544ad21c..7b3127774d 100644 --- a/hypothesis-python/tests/array_api/test_pretty.py +++ b/hypothesis-python/tests/array_api/test_pretty.py @@ -12,6 +12,8 @@ import pytest +from tests.array_api.common import MIN_VER_FOR_COMPLEX + @pytest.mark.parametrize( "name", @@ -26,7 +28,9 @@ "unsigned_integer_dtypes", "floating_dtypes", "real_dtypes", - pytest.param("complex_dtypes", marks=pytest.mark.xp_min_version("draft")), + pytest.param( + "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) + ), "valid_tuple_axes", "broadcastable_shapes", "mutually_broadcastable_shapes", @@ -58,7 +62,9 @@ def test_namespaced_methods_meta(xp, xps, name): ("unsigned_integer_dtypes", []), ("floating_dtypes", []), ("real_dtypes", []), - pytest.param("complex_dtypes", [], marks=pytest.mark.xp_min_version("draft")), + pytest.param( + "complex_dtypes", [], marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) + ), ("valid_tuple_axes", [0]), ("broadcastable_shapes", [()]), ("mutually_broadcastable_shapes", [3]), diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index e444782f5b..1a88f89d5f 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -21,6 +21,7 @@ api_version_gt, ) +from tests.array_api.common import MIN_VER_FOR_COMPLEX from tests.common.debug import assert_all_examples, find_any, minimal @@ -66,7 +67,7 @@ def test_can_generate_real_dtypes(xp, xps): assert_all_examples(xps.real_dtypes(), lambda dtype: dtype in real_dtypes) -@pytest.mark.xp_min_version("draft") +@pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) def test_can_generate_complex_dtypes(xp, xps): complex_dtypes = [getattr(xp, name) for name in COMPLEX_NAMES] assert_all_examples(xps.complex_dtypes(), lambda dtype: dtype in complex_dtypes) From 751edc3ee42550e5e2f5cb414604f875b8c183b4 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 12:39:05 +0100 Subject: [PATCH 36/62] `find_any()` testing and refactoring for `test_scalar_dtypes.py` --- .../tests/array_api/test_scalar_dtypes.py | 84 +++++++++++-------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index 1a88f89d5f..bd49396935 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -25,52 +25,62 @@ from tests.common.debug import assert_all_examples, find_any, minimal -def test_can_generate_scalar_dtypes(xp, xps): +@pytest.mark.parametrize( + ("strat_name", "dtype_names"), + [ + ("integer_dtypes", INT_NAMES), + ("unsigned_integer_dtypes", UINT_NAMES), + ("floating_dtypes", FLOAT_NAMES), + ("real_dtypes", REAL_NAMES), + pytest.param( + "complex_dtypes", + COMPLEX_NAMES, + marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX), + ), + ], +) +def test_all_generated_dtypes_are_of_group(xp, xps, strat_name, dtype_names): + strat_func = getattr(xps, strat_name) + dtypes = [getattr(xp, n) for n in dtype_names] + assert_all_examples(strat_func(), lambda dtype: dtype in dtypes) + + +def test_all_generated_scalar_dtypes_are_scalar(xp, xps): if api_version_gt(xps.api_version, "2021.12"): - dtypes = [getattr(xp, name) for name in DTYPE_NAMES] + dtypes = [getattr(xp, n) for n in DTYPE_NAMES] else: - dtypes = [getattr(xp, name) for name in ("bool",) + REAL_NAMES] + dtypes = [getattr(xp, n) for n in ("bool",) + REAL_NAMES] assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes) -def test_can_generate_boolean_dtypes(xp, xps): - assert_all_examples(xps.boolean_dtypes(), lambda dtype: dtype == xp.bool) - - -def test_can_generate_numeric_dtypes(xp, xps): +def test_all_generated_numeric_dtypes_are_numeric(xp, xps): if api_version_gt(xps.api_version, "2021.12"): - numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES] + dtypes = [getattr(xp, n) for n in NUMERIC_NAMES] else: - numeric_dtypes = [getattr(xp, name) for name in REAL_NAMES] - assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in numeric_dtypes) - - -def test_can_generate_integer_dtypes(xp, xps): - int_dtypes = [getattr(xp, name) for name in INT_NAMES] - assert_all_examples(xps.integer_dtypes(), lambda dtype: dtype in int_dtypes) + dtypes = [getattr(xp, n) for n in REAL_NAMES] + assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in dtypes) -def test_can_generate_unsigned_integer_dtypes(xp, xps): - uint_dtypes = [getattr(xp, name) for name in UINT_NAMES] - assert_all_examples( - xps.unsigned_integer_dtypes(), lambda dtype: dtype in uint_dtypes - ) - - -def test_can_generate_floating_dtypes(xp, xps): - float_dtypes = [getattr(xp, name) for name in FLOAT_NAMES] - assert_all_examples(xps.floating_dtypes(), lambda dtype: dtype in float_dtypes) - - -def test_can_generate_real_dtypes(xp, xps): - real_dtypes = [getattr(xp, name) for name in REAL_NAMES] - assert_all_examples(xps.real_dtypes(), lambda dtype: dtype in real_dtypes) - - -@pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) -def test_can_generate_complex_dtypes(xp, xps): - complex_dtypes = [getattr(xp, name) for name in COMPLEX_NAMES] - assert_all_examples(xps.complex_dtypes(), lambda dtype: dtype in complex_dtypes) +@pytest.mark.parametrize( + ("strat_name", "dtype_name"), + [ + *[("scalar_dtypes", n) for n in DTYPE_NAMES], + *[("numeric_dtypes", n) for n in NUMERIC_NAMES], + *[("integer_dtypes", n) for n in INT_NAMES], + *[("unsigned_integer_dtypes", n) for n in UINT_NAMES], + *[("floating_dtypes", n) for n in FLOAT_NAMES], + *[("real_dtypes", n) for n in REAL_NAMES], + *[("complex_dtypes", n) for n in COMPLEX_NAMES], + ], +) +def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name): + if dtype_name.startswith("complex") and api_version_gt( + MIN_VER_FOR_COMPLEX, xps.api_version + ): + pytest.skip(f"requires api_version=>{MIN_VER_FOR_COMPLEX}") + strat_func = getattr(xps, strat_name) + dtype = getattr(xp, dtype_name) + find_any(strat_func(), lambda d: d == dtype) def test_minimise_scalar_dtypes(xp, xps): From 5a67c58dd6aa1b96ecb1560498632465595b99ca Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 13:07:01 +0100 Subject: [PATCH 37/62] Test case for sizes-as-int in `xps.complex_dtypes()` --- hypothesis-python/tests/array_api/test_scalar_dtypes.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index bd49396935..954489f71b 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -93,6 +93,9 @@ def test_minimise_scalar_dtypes(xp, xps): ("integer_dtypes", 8), ("unsigned_integer_dtypes", 8), ("floating_dtypes", 32), + pytest.param( + "complex_dtypes", 64, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) + ), ], ) def test_can_specify_sizes_as_an_int(xp, xps, strat_name, sizes): From 7224514c696b945a1a9b1d18dc53c6dc828e01b3 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 13:30:35 +0100 Subject: [PATCH 38/62] docs: update `xps` hack in docs, list `xps.real_dtypes()` --- hypothesis-python/docs/conf.py | 10 ++++++++-- hypothesis-python/docs/numpy.rst | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/docs/conf.py b/hypothesis-python/docs/conf.py index 22117c4336..2498328bc9 100644 --- a/hypothesis-python/docs/conf.py +++ b/hypothesis-python/docs/conf.py @@ -57,10 +57,16 @@ def setup(app): app.tags.add("has_release_file") # patch in mock array_api namespace so we can autodoc it - from hypothesis.extra.array_api import make_strategies_namespace, mock_xp + from hypothesis.extra.array_api import ( + RELEASED_VERSIONS, + make_strategies_namespace, + mock_xp, + ) mod = types.ModuleType("xps") - mod.__dict__.update(make_strategies_namespace(mock_xp).__dict__) + mod.__dict__.update( + make_strategies_namespace(mock_xp, api_version=RELEASED_VERSIONS[-1]).__dict__ + ) assert "xps" not in sys.modules sys.modules["xps"] = mod diff --git a/hypothesis-python/docs/numpy.rst b/hypothesis-python/docs/numpy.rst index 8f592e1107..eb8e6d4618 100644 --- a/hypothesis-python/docs/numpy.rst +++ b/hypothesis-python/docs/numpy.rst @@ -83,9 +83,12 @@ standard semantics and returning objects from the ``xp`` module: scalar_dtypes, boolean_dtypes, numeric_dtypes, + real_dtypes, integer_dtypes, unsigned_integer_dtypes, floating_dtypes, + .. + TODO: for next released xp version, include complex_dtypes here valid_tuple_axes, broadcastable_shapes, mutually_broadcastable_shapes, From ecb8057728622f11e9b0fc869f15945714aad5bc Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 13:33:25 +0100 Subject: [PATCH 39/62] Update `make_strategies_namespace()` docstring --- hypothesis-python/src/hypothesis/extra/array_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index d0f84d2c47..376b516bac 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -863,7 +863,11 @@ def make_strategies_namespace( """Creates a strategies namespace for the given array module. * ``xp`` is the Array API library to automatically pass to the namespaced methods. - * ``api_version`` TODO + * ``api_version`` is the version of the Array API which the returned + strategies namespace should conform to. If ``None``, the latest API + version which ``xp`` supports will be inferred. If a version string in the + ``YYYY.MM`` format, the strategies namespace will conform to that version + if supported. A :obj:`python:types.SimpleNamespace` is returned which contains all the strategy methods in this module but without requiring the ``xp`` argument. @@ -874,11 +878,13 @@ def make_strategies_namespace( >>> from numpy import array_api as xp >>> xps = make_strategies_namespace(xp) + >>> xps.api_version + '2021.12' # this will depend on the version of NumPy installed >>> x = xps.arrays(xp.int8, (2, 3)).example() >>> x Array([[-8, 6, 3], [-6, 4, 6]], dtype=int8) - >>> x.__array_namespace__() is xp TODO + >>> x.__array_namespace__() is xp True """ From cdfdbd6dbf649be5eebb8e55f8c7ca3e29ec737f Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 18:48:08 +0100 Subject: [PATCH 40/62] `tests/array_api/` docstrings and re-org --- .../array_api/test_argument_validation.py | 3 +- .../tests/array_api/test_partial_adoptors.py | 57 ++------------- .../tests/array_api/test_scalar_dtypes.py | 6 ++ .../array_api/test_strategies_namespace.py | 69 +++++++++++++++++-- 4 files changed, 77 insertions(+), 58 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index ad73c49665..3a14ee9987 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -229,6 +229,7 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs): @pytest.mark.parametrize("api_version", [..., "latest", "1970.01", 42]) -def test_make_namespace_raise_invalid_argument(xp, api_version): +def test_make_strategies_namespace_raise_invalid_argument(xp, api_version): + """Function raises helpful error with invalid arguments.""" with pytest.raises(InvalidArgument): make_strategies_namespace(xp, api_version=api_version) diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index adf2c568a2..7bf9908b02 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -11,7 +11,7 @@ from copy import copy from functools import lru_cache from types import SimpleNamespace -from typing import List, Optional, Tuple +from typing import Tuple import pytest @@ -22,9 +22,7 @@ DTYPE_NAMES, FLOAT_NAMES, INT_NAMES, - RELEASED_VERSIONS, UINT_NAMES, - NominalVersion, make_strategies_namespace, mock_xp, ) @@ -125,61 +123,16 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data): data.draw(func()) -class MockArray: - def __init__(self, supported_versions: Tuple[NominalVersion, ...]): - assert len(set(supported_versions)) == len(supported_versions) # sanity check - self.supported_versions = supported_versions - - def __array_namespace__(self, *, api_version: Optional[NominalVersion] = None): - if api_version is not None and api_version not in self.supported_versions: - raise - return SimpleNamespace( - __name__="foopy", zeros=lambda _: MockArray(self.supported_versions) - ) - - -version_permutations: List[Tuple[NominalVersion, ...]] = [ - RELEASED_VERSIONS[:i] for i in range(1, len(RELEASED_VERSIONS) + 1) -] - - -@pytest.mark.parametrize( - "supported_versions", - version_permutations, - ids=lambda supported_versions: "-".join(supported_versions), -) -def test_version_inferrence(supported_versions): - xp = MockArray(supported_versions).__array_namespace__() - xps = make_strategies_namespace(xp) - assert xps.api_version == supported_versions[-1] - - -def test_raises_on_inferring_with_no_supported_versions(): - xp = MockArray(()).__array_namespace__() - with pytest.raises(InvalidArgument): - xps = make_strategies_namespace(xp) - - -@pytest.mark.parametrize( - ("api_version", "supported_versions"), - [pytest.param(p[-1], p[:-1], id=p[-1]) for p in version_permutations], -) -def test_warns_on_specifying_unsupported_version(api_version, supported_versions): - xp = MockArray(supported_versions).__array_namespace__() - xp.zeros = None - with pytest.warns(HypothesisWarning): - xps = make_strategies_namespace(xp, api_version=api_version) - assert xps.api_version == api_version - - def test_raises_on_inferring_with_no_zeros_func(): + """When xp has no zeros(), inferring api_version raises helpful error.""" xp = make_mock_xp(exclude=("zeros",)) with pytest.raises(InvalidArgument, match="has no function"): - xps = make_strategies_namespace(xp) + make_strategies_namespace(xp) def test_raises_on_erroneous_zeros_func(): + """When xp has erroneous zeros(), inferring api_version raises helpful error.""" xp = make_mock_xp() xp.zeros = None with pytest.raises(InvalidArgument): - xps = make_strategies_namespace(xp) + make_strategies_namespace(xp) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index 954489f71b..c6dcd3c9d5 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -40,12 +40,14 @@ ], ) def test_all_generated_dtypes_are_of_group(xp, xps, strat_name, dtype_names): + """Strategy only generates expected dtypes.""" strat_func = getattr(xps, strat_name) dtypes = [getattr(xp, n) for n in dtype_names] assert_all_examples(strat_func(), lambda dtype: dtype in dtypes) def test_all_generated_scalar_dtypes_are_scalar(xp, xps): + """Strategy only generates scalar dtypes.""" if api_version_gt(xps.api_version, "2021.12"): dtypes = [getattr(xp, n) for n in DTYPE_NAMES] else: @@ -54,6 +56,7 @@ def test_all_generated_scalar_dtypes_are_scalar(xp, xps): def test_all_generated_numeric_dtypes_are_numeric(xp, xps): + """Strategy only generates numeric dtypes.""" if api_version_gt(xps.api_version, "2021.12"): dtypes = [getattr(xp, n) for n in NUMERIC_NAMES] else: @@ -74,6 +77,7 @@ def test_all_generated_numeric_dtypes_are_numeric(xp, xps): ], ) def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name): + """Strategy generates every expected dtype.""" if dtype_name.startswith("complex") and api_version_gt( MIN_VER_FOR_COMPLEX, xps.api_version ): @@ -84,6 +88,7 @@ def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name): def test_minimise_scalar_dtypes(xp, xps): + """Strategy minimizes to bool dtype.""" assert minimal(xps.scalar_dtypes()) == xp.bool @@ -99,6 +104,7 @@ def test_minimise_scalar_dtypes(xp, xps): ], ) def test_can_specify_sizes_as_an_int(xp, xps, strat_name, sizes): + """Strategy treats ints as a single size.""" strat_func = getattr(xps, strat_name) strat = strat_func(sizes=sizes) find_any(strat) diff --git a/hypothesis-python/tests/array_api/test_strategies_namespace.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py index ce0671f441..88e148f29f 100644 --- a/hypothesis-python/tests/array_api/test_strategies_namespace.py +++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py @@ -9,22 +9,28 @@ # obtain one at https://mozilla.org/MPL/2.0/. from types import SimpleNamespace +from typing import Tuple from weakref import WeakValueDictionary import pytest +from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra import array_api from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, + RELEASED_VERSIONS, + List, + NominalVersion, + Optional, make_strategies_namespace, mock_xp, ) from hypothesis.strategies import SearchStrategy -pytestmark = pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") - +@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") def test_caching(xp, monkeypatch): + """Caches namespaces respective to arguments.""" try: hash(xp) except TypeError: @@ -43,10 +49,63 @@ def test_caching(xp, monkeypatch): assert len(array_api._args_to_xps) == 0 -def test_complex_dtypes_raises_on_first_version(): - first_xps = make_strategies_namespace(mock_xp, api_version=NOMINAL_VERSIONS[0]) - with pytest.raises(AttributeError): +@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") +def test_complex_dtypes_raises_on_2021_12(): + """Accessing complex_dtypes() for 2021.12 strategy namespace raises helpful + error, but accessing on future versions returns expected strategy.""" + first_xps = make_strategies_namespace(mock_xp, api_version="2021.12") + with pytest.raises(AttributeError, match="attempted to access"): first_xps.complex_dtypes() for api_version in NOMINAL_VERSIONS[1:]: xps = make_strategies_namespace(mock_xp, api_version=api_version) assert isinstance(xps.complex_dtypes(), SearchStrategy) + + +class MockArray: + def __init__(self, supported_versions: Tuple[NominalVersion, ...]): + assert len(set(supported_versions)) == len(supported_versions) # sanity check + self.supported_versions = supported_versions + + def __array_namespace__(self, *, api_version: Optional[NominalVersion] = None): + if api_version is not None and api_version not in self.supported_versions: + raise + return SimpleNamespace( + __name__="foopy", zeros=lambda _: MockArray(self.supported_versions) + ) + + +version_permutations: List[Tuple[NominalVersion, ...]] = [ + RELEASED_VERSIONS[:i] for i in range(1, len(RELEASED_VERSIONS) + 1) +] + + +@pytest.mark.parametrize( + "supported_versions", + version_permutations, + ids=lambda supported_versions: "-".join(supported_versions), +) +def test_version_inferrence(supported_versions): + """Latest supported api_version is inferred.""" + xp = MockArray(supported_versions).__array_namespace__() + xps = make_strategies_namespace(xp) + assert xps.api_version == supported_versions[-1] + + +def test_raises_on_inferring_with_no_supported_versions(): + """When xp supports no versions, inferring api_version raises helpful error.""" + xp = MockArray(()).__array_namespace__() + with pytest.raises(InvalidArgument): + xps = make_strategies_namespace(xp) + + +@pytest.mark.parametrize( + ("api_version", "supported_versions"), + [pytest.param(p[-1], p[:-1], id=p[-1]) for p in version_permutations], +) +def test_warns_on_specifying_unsupported_version(api_version, supported_versions): + """Specifying an api_version which xp does not support executes with a warning.""" + xp = MockArray(supported_versions).__array_namespace__() + xp.zeros = None + with pytest.warns(HypothesisWarning): + xps = make_strategies_namespace(xp, api_version=api_version) + assert xps.api_version == api_version From b87ad8d08879772a91bf932d05e1ae1f5c871c5e Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 20 Sep 2022 18:52:11 +0100 Subject: [PATCH 41/62] Mark params with complex in `test_strategy_can_generate_every_dtype` --- .../tests/array_api/test_scalar_dtypes.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index c6dcd3c9d5..09831eadd1 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -64,24 +64,27 @@ def test_all_generated_numeric_dtypes_are_numeric(xp, xps): assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in dtypes) +def skipif_unsupported_complex(strat_name, dtype_name): + if not dtype_name.startswith("complex"): + return strat_name, dtype_name + mark = pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX) + return pytest.param(strat_name, dtype_name, marks=mark) + + @pytest.mark.parametrize( ("strat_name", "dtype_name"), [ - *[("scalar_dtypes", n) for n in DTYPE_NAMES], - *[("numeric_dtypes", n) for n in NUMERIC_NAMES], + *[skipif_unsupported_complex("scalar_dtypes", n) for n in DTYPE_NAMES], + *[skipif_unsupported_complex("numeric_dtypes", n) for n in NUMERIC_NAMES], *[("integer_dtypes", n) for n in INT_NAMES], *[("unsigned_integer_dtypes", n) for n in UINT_NAMES], *[("floating_dtypes", n) for n in FLOAT_NAMES], *[("real_dtypes", n) for n in REAL_NAMES], - *[("complex_dtypes", n) for n in COMPLEX_NAMES], + *[skipif_unsupported_complex("complex_dtypes", n) for n in COMPLEX_NAMES], ], ) def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name): """Strategy generates every expected dtype.""" - if dtype_name.startswith("complex") and api_version_gt( - MIN_VER_FOR_COMPLEX, xps.api_version - ): - pytest.skip(f"requires api_version=>{MIN_VER_FOR_COMPLEX}") strat_func = getattr(xps, strat_name) dtype = getattr(xp, dtype_name) find_any(strat_func(), lambda d: d == dtype) From 0f88cdf6e717d8a5100f40b23747636b8cda4a2d Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 22 Sep 2022 15:35:54 +0100 Subject: [PATCH 42/62] Use `__array_api_version__` for version inferrence --- .../src/hypothesis/extra/array_api.py | 56 ++++++++----------- hypothesis-python/tests/array_api/conftest.py | 14 ++++- .../tests/array_api/test_partial_adoptors.py | 16 +++--- .../array_api/test_strategies_namespace.py | 56 ------------------- 4 files changed, 44 insertions(+), 98 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 376b516bac..2ddd79ca0d 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -8,7 +8,6 @@ # 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 contextlib import math import sys from numbers import Real @@ -865,9 +864,9 @@ def make_strategies_namespace( * ``xp`` is the Array API library to automatically pass to the namespaced methods. * ``api_version`` is the version of the Array API which the returned strategies namespace should conform to. If ``None``, the latest API - version which ``xp`` supports will be inferred. If a version string in the - ``YYYY.MM`` format, the strategies namespace will conform to that version - if supported. + version which ``xp`` supports will be inferred from ``xp.__array_api_version__``. + If a version string in the ``YYYY.MM`` format, the strategies namespace + will conform to that version if supported. A :obj:`python:types.SimpleNamespace` is returned which contains all the strategy methods in this module but without requiring the ``xp`` argument. @@ -876,10 +875,11 @@ def make_strategies_namespace( .. code-block:: pycon - >>> from numpy import array_api as xp + >>> xp.__array_api_version__ # xp is your desired array library + '2021.12' >>> xps = make_strategies_namespace(xp) >>> xps.api_version - '2021.12' # this will depend on the version of NumPy installed + '2021.12' >>> x = xps.arrays(xp.int8, (2, 3)).example() >>> x Array([[-8, 6, 3], @@ -895,41 +895,32 @@ def make_strategies_namespace( else: return namespace + not_available_msg = ( + "If the standard version you want is not available, please ensure " + "you're using the latest version of Hypothesis, then open an issue if " + "one doesn't already exist." + ) check_argument( api_version is None or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), f"{api_version=}, but api_version must be None, or a valid version " - f"string {RELEASED_VERSIONS}. If the standard version you want is not " - "available, please ensure you're using the latest version of " - "Hypothesis, then open an issue if one doesn't already exist.", + f"string {RELEASED_VERSIONS}. {not_available_msg}", ) if api_version is None: - # When api_version=None, we infer the most recent API version for which - # the passed xp is valid. We go through the released versions in - # descending order, passing them to x.__array_namespace__() until no - # errors are raised, thus inferring that specific api_version is - # supported. If errors are raised for all released versions, we raise - # our own useful error. check_argument( - hasattr(xp, "zeros"), - f"Array module {xp.__name__} has no function zeros(), which is " - "required when inferring api_version.", + hasattr(xp, "__array_api_version__"), + f"Array module {xp.__name__} has no attribute __array_api_version__, " + "which is required when inferring api_version. If you believe " + f"{xp.__name__} is indeed an Array API module, try explicitly " + "passing an api_version.", ) - errmsg = ( - f"Could not infer any api_version which module {xp.__name__} " - f"supports. If you believe {xp.__name__} is indeed an Array API " - "module, try explicitly passing an api_version." + check_argument( + isinstance(xp.__array_api_version__, str) + and xp.__array_api_version__ in RELEASED_VERSIONS, + f"{xp.__array_api_version__=}, but xp.__array_api_version__ must " + f"be a valid version string {RELEASED_VERSIONS}. {not_available_msg}", ) - try: - array = xp.zeros(1) - except Exception: - raise InvalidArgument(errmsg) - for api_version in reversed(RELEASED_VERSIONS): - with contextlib.suppress(Exception): - xp = array.__array_namespace__(api_version=api_version) - break # i.e. a valid xp and api_version has been inferred - else: - raise InvalidArgument(errmsg) + api_version = xp.__array_api_version__ try: array = xp.zeros(1) array.__array_namespace__() @@ -1132,6 +1123,7 @@ def mock_finfo(dtype: DataType) -> FloatInfo: mock_xp = SimpleNamespace( __name__="mock", + __array_api_version__="2021.12", # Data types int8=np.int8, int16=np.int16, diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index d80c4fd51d..7642613538 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -14,7 +14,7 @@ import pytest -from hypothesis.errors import HypothesisWarning +from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, NominalVersion, @@ -52,8 +52,13 @@ except KeyError: pass else: - xps = make_strategies_namespace(xp, api_version=api_version) - xp_and_xps_pairs.append((xp, xps)) + # TODO: think about failing gracefully more, apply for similar instances + try: + xps = make_strategies_namespace(xp, api_version=api_version) + except InvalidArgument as e: + warnings.warn(e) + else: + xp_and_xps_pairs.append((xp, xps)) elif test_xp_option == "all": if len(name_to_entry_point) == 0: raise ValueError( @@ -62,16 +67,19 @@ xp_and_xps_pairs = [(mock_xp, mock_xps)] for name, ep in name_to_entry_point.items(): xp = ep.load() + # TODO xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs.append((xp, xps)) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() + # TODO xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] else: try: xp = import_module(test_xp_option) + # TODO xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] except ImportError as e: diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py index 7bf9908b02..2f0f410626 100644 --- a/hypothesis-python/tests/array_api/test_partial_adoptors.py +++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py @@ -123,16 +123,18 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data): data.draw(func()) -def test_raises_on_inferring_with_no_zeros_func(): - """When xp has no zeros(), inferring api_version raises helpful error.""" - xp = make_mock_xp(exclude=("zeros",)) - with pytest.raises(InvalidArgument, match="has no function"): +def test_raises_on_inferring_with_no_dunder_version(): + """When xp has no __array_api_version__, inferring api_version raises + helpful error.""" + xp = make_mock_xp(exclude=("__array_api_version__",)) + with pytest.raises(InvalidArgument, match="has no attribute"): make_strategies_namespace(xp) -def test_raises_on_erroneous_zeros_func(): - """When xp has erroneous zeros(), inferring api_version raises helpful error.""" +def test_raises_on_invalid_dunder_version(): + """When xp has invalid __array_api_version__, inferring api_version raises + helpful error.""" xp = make_mock_xp() - xp.zeros = None + xp.__array_api_version__ = None with pytest.raises(InvalidArgument): make_strategies_namespace(xp) diff --git a/hypothesis-python/tests/array_api/test_strategies_namespace.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py index 88e148f29f..83bfeefbb0 100644 --- a/hypothesis-python/tests/array_api/test_strategies_namespace.py +++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py @@ -9,19 +9,13 @@ # obtain one at https://mozilla.org/MPL/2.0/. from types import SimpleNamespace -from typing import Tuple from weakref import WeakValueDictionary import pytest -from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.extra import array_api from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, - RELEASED_VERSIONS, - List, - NominalVersion, - Optional, make_strategies_namespace, mock_xp, ) @@ -59,53 +53,3 @@ def test_complex_dtypes_raises_on_2021_12(): for api_version in NOMINAL_VERSIONS[1:]: xps = make_strategies_namespace(mock_xp, api_version=api_version) assert isinstance(xps.complex_dtypes(), SearchStrategy) - - -class MockArray: - def __init__(self, supported_versions: Tuple[NominalVersion, ...]): - assert len(set(supported_versions)) == len(supported_versions) # sanity check - self.supported_versions = supported_versions - - def __array_namespace__(self, *, api_version: Optional[NominalVersion] = None): - if api_version is not None and api_version not in self.supported_versions: - raise - return SimpleNamespace( - __name__="foopy", zeros=lambda _: MockArray(self.supported_versions) - ) - - -version_permutations: List[Tuple[NominalVersion, ...]] = [ - RELEASED_VERSIONS[:i] for i in range(1, len(RELEASED_VERSIONS) + 1) -] - - -@pytest.mark.parametrize( - "supported_versions", - version_permutations, - ids=lambda supported_versions: "-".join(supported_versions), -) -def test_version_inferrence(supported_versions): - """Latest supported api_version is inferred.""" - xp = MockArray(supported_versions).__array_namespace__() - xps = make_strategies_namespace(xp) - assert xps.api_version == supported_versions[-1] - - -def test_raises_on_inferring_with_no_supported_versions(): - """When xp supports no versions, inferring api_version raises helpful error.""" - xp = MockArray(()).__array_namespace__() - with pytest.raises(InvalidArgument): - xps = make_strategies_namespace(xp) - - -@pytest.mark.parametrize( - ("api_version", "supported_versions"), - [pytest.param(p[-1], p[:-1], id=p[-1]) for p in version_permutations], -) -def test_warns_on_specifying_unsupported_version(api_version, supported_versions): - """Specifying an api_version which xp does not support executes with a warning.""" - xp = MockArray(supported_versions).__array_namespace__() - xp.zeros = None - with pytest.warns(HypothesisWarning): - xps = make_strategies_namespace(xp, api_version=api_version) - assert xps.api_version == api_version From edd8f87247d01f2d910f7e59f0f5e207d9d4093c Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 22 Sep 2022 16:44:29 +0100 Subject: [PATCH 43/62] Handle modules lacking `__array_api_version__` in `tests/array_api/` --- hypothesis-python/tests/array_api/conftest.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 7642613538..6ea572ad59 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -11,6 +11,8 @@ import warnings from importlib import import_module from os import getenv +from types import ModuleType, SimpleNamespace +from typing import Tuple import pytest @@ -27,6 +29,7 @@ # See README.md in regards to the env variables test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default") + test_version_option = getenv("HYPOTHESIS_TEST_ARRAY_API_VERSION", "default") if test_version_option != "default" and test_version_option not in NOMINAL_VERSIONS: raise ValueError( @@ -38,10 +41,18 @@ mock_xps = make_strategies_namespace(mock_xp, api_version=mock_version) api_version = None if test_version_option == "default" else test_version_option + +class InvalidArgumentWarning(UserWarning): + """Custom warning so we can bypass our global capturing""" + + name_to_entry_point = installed_array_modules() +xp_and_xps_pairs: Tuple[ModuleType, SimpleNamespace] = [] with warnings.catch_warnings(): - # We ignore all warnings here as many array modules warn on import + # We ignore all warnings here as many array modules warn on import. Ideally + # we would just ignore ImportWarning, but no one seems to use it! warnings.simplefilter("ignore") + warnings.simplefilter("default", category=InvalidArgumentWarning) # We go through the steps described in README.md to define `xp_xps_pairs`, # which contains the array module(s) to be run against the test suite, along # with their respective strategy namespaces. @@ -52,11 +63,10 @@ except KeyError: pass else: - # TODO: think about failing gracefully more, apply for similar instances try: xps = make_strategies_namespace(xp, api_version=api_version) except InvalidArgument as e: - warnings.warn(e) + warnings.warn(str(e), InvalidArgumentWarning) else: xp_and_xps_pairs.append((xp, xps)) elif test_xp_option == "all": @@ -67,19 +77,20 @@ xp_and_xps_pairs = [(mock_xp, mock_xps)] for name, ep in name_to_entry_point.items(): xp = ep.load() - # TODO - xps = make_strategies_namespace(xp, api_version=api_version) - xp_and_xps_pairs.append((xp, xps)) + try: + xps = make_strategies_namespace(xp, api_version=api_version) + except InvalidArgument as e: + warnings.warn(str(e), InvalidArgumentWarning) + else: + xp_and_xps_pairs.append((xp, xps)) elif test_xp_option in name_to_entry_point.keys(): ep = name_to_entry_point[test_xp_option] xp = ep.load() - # TODO xps = make_strategies_namespace(xp, api_version=api_version) - xp_and_xps_pairs = [(xp, xps)] + xp_and_xps_pairs.append(xp, xps) else: try: xp = import_module(test_xp_option) - # TODO xps = make_strategies_namespace(xp, api_version=api_version) xp_and_xps_pairs = [(xp, xps)] except ImportError as e: From 53fcd6f2cc38d438d4dcd76fe7e411260f14ed54 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 22 Sep 2022 16:55:43 +0100 Subject: [PATCH 44/62] Just use the mock for `tests/array_api/` by default --- hypothesis-python/tests/array_api/README.md | 3 +-- hypothesis-python/tests/array_api/conftest.py | 11 ----------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md index 634cf738be..bc2f4d1aa2 100644 --- a/hypothesis-python/tests/array_api/README.md +++ b/hypothesis-python/tests/array_api/README.md @@ -13,7 +13,7 @@ You can test other array modules which adopt the Array API via the `HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized options: -* `"default"`: uses the mock, and `numpy.array_api` if available. +* `"default"`: uses the mock. * `"all"`: uses all array modules found via entry points, _and_ the mock. If neither of these, the test suite will then try resolve the variable like so: @@ -34,7 +34,6 @@ The former method is more ergonomic, but as entry points are optional for adopting the Array API, you will need to use the latter method for libraries that opt-out. - ## Running against different API versions You can specify the `api_version` to use when testing array modules via the diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 6ea572ad59..4e59b70f5f 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -58,17 +58,6 @@ class InvalidArgumentWarning(UserWarning): # with their respective strategy namespaces. if test_xp_option == "default": xp_and_xps_pairs = [(mock_xp, mock_xps)] - try: - xp = name_to_entry_point["numpy"].load() - except KeyError: - pass - else: - try: - xps = make_strategies_namespace(xp, api_version=api_version) - except InvalidArgument as e: - warnings.warn(str(e), InvalidArgumentWarning) - else: - xp_and_xps_pairs.append((xp, xps)) elif test_xp_option == "all": if len(name_to_entry_point) == 0: raise ValueError( From 97e516f792bc490f8b4dbd81541027a0c168e927 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 22 Sep 2022 16:59:27 +0100 Subject: [PATCH 45/62] `conftest.py`: Fix entrypoint logic, use `else` in a `try-except` --- hypothesis-python/tests/array_api/conftest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 4e59b70f5f..3be9059c5b 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -76,18 +76,19 @@ class InvalidArgumentWarning(UserWarning): ep = name_to_entry_point[test_xp_option] xp = ep.load() xps = make_strategies_namespace(xp, api_version=api_version) - xp_and_xps_pairs.append(xp, xps) + xp_and_xps_pairs = [(xp, xps)] else: try: xp = import_module(test_xp_option) - xps = make_strategies_namespace(xp, api_version=api_version) - xp_and_xps_pairs = [(xp, xps)] except ImportError as e: raise ValueError( f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid " "option ('default' or 'all'), name of an available entry point, " "or a valid import path." ) from e + else: + xps = make_strategies_namespace(xp, api_version=api_version) + xp_and_xps_pairs = [(xp, xps)] def pytest_generate_tests(metafunc): From 87b643b51f9251707614f0cdfb91926fa7c91fe7 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 14:04:41 +0100 Subject: [PATCH 46/62] Note decisions in complex branch of `array_api._from_dtype()` --- hypothesis-python/src/hypothesis/extra/array_api.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 2ddd79ca0d..f33d96f549 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -308,6 +308,11 @@ def check_valid_minmax(prefix, val, info_obj): "currently required for xps.from_dtype() to work with " "any complex dtype." ) + # Ideally we would infer allow_subnormal with a complex array here, just + # in case the array library has different FTZ behaviour between complex + # and float arrays. Unfortunately the spec currently has no mechanism to + # extract both real and imaj components, but should in the future. + # See https://github.com/data-apis/array-api/pull/427 kw = { "allow_nan": allow_nan, "allow_infinity": allow_infinity, @@ -317,7 +322,11 @@ def check_valid_minmax(prefix, val, info_obj): floats = _from_dtype(xp, api_version, xp.float32, **kw) else: floats = _from_dtype(xp, api_version, xp.float64, **kw) - + # Due to the aforementioned lack of complex dtype inspection, along with + # no mechanism to separate real and imaj components, we lean on + # st.builds() here. Once both issues resolves, in the future we should + # lean on st.complex_numbers() - we could then maybe support min/max + # magnitude arguments! return st.builds(complex, floats, floats) From 1c22c6c2b83b2fb5dbc44ecf8b7395db834ffc59 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 15:08:20 +0100 Subject: [PATCH 47/62] Add `RELEASE.rst` --- hypothesis-python/RELEASE.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..50652f11e8 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,11 @@ +RELEASE_TYPE: minor + +This release updates :func:`~hypothesis.extra.array_api.make_strategies_namespace` +by introducing a ``api_version`` argument, defaulting to ``None``. If a `valid +version string `_, +the returned strategies namespace should conform to the specified version. If +``None``, the version of the passed Array API module ``xp`` is inferred and +conformed to. + +This release also introduces :func:`xps.real_dtypes`, which generates +all real-valued dtypes (i.e. integers and floats) specified in the Array API. From 17b1a760b2c313cbea764c0979f0b3d3c151640f Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 15:12:21 +0100 Subject: [PATCH 48/62] Clarify "real-valued" for `xps.floating_dtypes()` docstring --- hypothesis-python/src/hypothesis/extra/array_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index f33d96f549..ca30e47834 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -205,11 +205,12 @@ def _from_dtype( :xp-ref:`valid dtype `. Compatible ``**kwargs`` are passed to the inferred strategy function for - integers and floats. This allows you to customise the min and max values, + integers, floats. This allows you to customise the min and max values, and exclude non-finite numbers. This is particularly useful when kwargs are passed through from :func:`arrays()`, as it seamlessly handles the ``width`` or other representable bounds for you. """ + # TODO: for next released xp version, add note for complex dtype support check_xp_attributes(xp, ["iinfo", "finfo"]) if isinstance(dtype, str): @@ -717,7 +718,7 @@ def _unsigned_integer_dtypes( def _floating_dtypes( xp: Any, *, sizes: Union[int, Sequence[int]] = (32, 64) ) -> st.SearchStrategy[DataType]: - """Return a strategy for floating-point dtype objects. + """Return a strategy for real-valued floating-point dtype objects. ``sizes`` contains the floating-point sizes in bits, defaulting to ``(32, 64)`` which covers all valid sizes. From bcf21174a3a53a9fc217c567dbc10b068e200cdc Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 16:48:26 +0100 Subject: [PATCH 49/62] Lint fix for raised error inside an except --- hypothesis-python/src/hypothesis/extra/array_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index ca30e47834..f0629b2be9 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -1041,11 +1041,11 @@ def __init__(self, **kwargs): def complex_dtypes(self): try: return self.__dict__["complex_dtypes"] - except KeyError: + except KeyError as e: raise AttributeError( "You attempted to access 'complex_dtypes', but it is not " f"available for api_version='{self.api_version}'." - ) + ) from e def __repr__(self): return ( From 56a59b4eb066c80a8d0f4e7424824c055fc260ad Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 16:58:41 +0100 Subject: [PATCH 50/62] Fix doc warning on commenting inside directive --- hypothesis-python/docs/numpy.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/docs/numpy.rst b/hypothesis-python/docs/numpy.rst index eb8e6d4618..c7e2be099b 100644 --- a/hypothesis-python/docs/numpy.rst +++ b/hypothesis-python/docs/numpy.rst @@ -75,6 +75,9 @@ The resulting namespace contains all our familiar strategies like :func:`~xps.arrays` and :func:`~xps.from_dtype`, but based on the Array API standard semantics and returning objects from the ``xp`` module: +.. + TODO: for next released xp version, include complex_dtypes here + .. automodule:: xps :members: from_dtype, @@ -87,8 +90,6 @@ standard semantics and returning objects from the ``xp`` module: integer_dtypes, unsigned_integer_dtypes, floating_dtypes, - .. - TODO: for next released xp version, include complex_dtypes here valid_tuple_axes, broadcastable_shapes, mutually_broadcastable_shapes, From 1ee6b4585a1af97dd6900f1e5141f24227577be1 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Fri, 23 Sep 2022 17:40:21 +0100 Subject: [PATCH 51/62] mypy fixes and ignores --- .../src/hypothesis/extra/array_api.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index f0629b2be9..bb6e6f6a44 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -26,6 +26,7 @@ Type, TypeVar, Union, + get_args, ) from warnings import warn from weakref import WeakValueDictionary @@ -69,7 +70,7 @@ assert sorted(RELEASED_VERSIONS) == list(RELEASED_VERSIONS) # sanity check NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) NominalVersion = Literal["2021.12", "draft"] -assert NominalVersion.__args__ == NOMINAL_VERSIONS # sanity check +assert get_args(NominalVersion) == NOMINAL_VERSIONS # sanity check def api_version_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: @@ -309,26 +310,26 @@ def check_valid_minmax(prefix, val, info_obj): "currently required for xps.from_dtype() to work with " "any complex dtype." ) + component_dtype = xp.float32 if dtype == xp.complex64 else xp.float64 # Ideally we would infer allow_subnormal with a complex array here, just # in case the array library has different FTZ behaviour between complex # and float arrays. Unfortunately the spec currently has no mechanism to # extract both real and imaj components, but should in the future. # See https://github.com/data-apis/array-api/pull/427 - kw = { - "allow_nan": allow_nan, - "allow_infinity": allow_infinity, - "allow_subnormal": allow_subnormal, - } - if dtype == xp.complex64: - floats = _from_dtype(xp, api_version, xp.float32, **kw) - else: - floats = _from_dtype(xp, api_version, xp.float64, **kw) + floats = _from_dtype( + xp, + api_version, + component_dtype, + allow_nan=allow_nan, + allow_infinity=allow_infinity, + allow_subnormal=allow_subnormal, + ) # Due to the aforementioned lack of complex dtype inspection, along with # no mechanism to separate real and imaj components, we lean on # st.builds() here. Once both issues resolves, in the future we should # lean on st.complex_numbers() - we could then maybe support min/max # magnitude arguments! - return st.builds(complex, floats, floats) + return st.builds(complex, floats, floats) # type: ignore[arg-type] class ArrayStrategy(st.SearchStrategy): @@ -650,7 +651,7 @@ def _numeric_dtypes( xp: Any, api_version: NominalVersion ) -> st.SearchStrategy[DataType]: """Return a strategy for all numeric dtype objects.""" - strat = _real_dtypes(xp) + strat: st.SearchStrategy[DataType] = _real_dtypes(xp) if api_version_gt(api_version, "2021.12"): strat |= _complex_dtypes(xp) return strat @@ -863,7 +864,7 @@ def indices( # Cache for make_strategies_namespace() -_args_to_xps = WeakValueDictionary() +_args_to_xps: WeakValueDictionary = WeakValueDictionary() def make_strategies_namespace( @@ -954,7 +955,7 @@ def from_dtype( ) -> st.SearchStrategy[Union[bool, int, float]]: return _from_dtype( xp, - api_version, + api_version, # type: ignore[arg-type] dtype, min_value=min_value, max_value=max_value, @@ -978,7 +979,7 @@ def arrays( ) -> st.SearchStrategy: return _arrays( xp, - api_version, + api_version, # type: ignore[arg-type] dtype, shape, elements=elements, @@ -988,7 +989,7 @@ def arrays( @defines_strategy() def scalar_dtypes() -> st.SearchStrategy[DataType]: - return _scalar_dtypes(xp, api_version) + return _scalar_dtypes(xp, api_version) # type: ignore[arg-type] @defines_strategy() def boolean_dtypes() -> st.SearchStrategy[DataType]: @@ -1000,7 +1001,7 @@ def real_dtypes() -> st.SearchStrategy[DataType]: @defines_strategy() def numeric_dtypes() -> st.SearchStrategy[DataType]: - return _numeric_dtypes(xp, api_version) + return _numeric_dtypes(xp, api_version) # type: ignore[arg-type] @defines_strategy() def integer_dtypes( From 8d4386dd3eef38bf86a2171a632460132c39b95a Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Mon, 26 Sep 2022 11:58:41 +0100 Subject: [PATCH 52/62] Remove a misleading and an unnecessary comment --- hypothesis-python/src/hypothesis/extra/array_api.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index bb6e6f6a44..a30c5be8f3 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -311,11 +311,7 @@ def check_valid_minmax(prefix, val, info_obj): "any complex dtype." ) component_dtype = xp.float32 if dtype == xp.complex64 else xp.float64 - # Ideally we would infer allow_subnormal with a complex array here, just - # in case the array library has different FTZ behaviour between complex - # and float arrays. Unfortunately the spec currently has no mechanism to - # extract both real and imaj components, but should in the future. - # See https://github.com/data-apis/array-api/pull/427 + floats = _from_dtype( xp, api_version, @@ -324,11 +320,7 @@ def check_valid_minmax(prefix, val, info_obj): allow_infinity=allow_infinity, allow_subnormal=allow_subnormal, ) - # Due to the aforementioned lack of complex dtype inspection, along with - # no mechanism to separate real and imaj components, we lean on - # st.builds() here. Once both issues resolves, in the future we should - # lean on st.complex_numbers() - we could then maybe support min/max - # magnitude arguments! + return st.builds(complex, floats, floats) # type: ignore[arg-type] From e7cee0c8204de66d55e385d267d7ca1ff80a3b70 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 09:42:05 +0100 Subject: [PATCH 53/62] Explain the why in `RELEASE.rst` --- hypothesis-python/RELEASE.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst index 50652f11e8..a35bb9dc92 100644 --- a/hypothesis-python/RELEASE.rst +++ b/hypothesis-python/RELEASE.rst @@ -1,11 +1,13 @@ RELEASE_TYPE: minor -This release updates :func:`~hypothesis.extra.array_api.make_strategies_namespace` -by introducing a ``api_version`` argument, defaulting to ``None``. If a `valid -version string `_, -the returned strategies namespace should conform to the specified version. If -``None``, the version of the passed Array API module ``xp`` is inferred and -conformed to. +In preparation for `future versions of the Array API standard +`__, +:func:`~hypothesis.extra.array_api.make_strategies_namespace` now accepts an +optional ``api_version`` argument, which determines the version conformed to by +the returned strategies namespace. If ``None``, the version of the passed array +module ``xp`` is inferred. -This release also introduces :func:`xps.real_dtypes`, which generates -all real-valued dtypes (i.e. integers and floats) specified in the Array API. +This release also introduces :func:`xps.real_dtypes`. This is currently +equivalent to the existing :func:`xps.numeric_dtypes` strategy, but exists +because the latter is expected to include complex numbers in the next version of +the standard. From 556aba432666668cdccd96fe0064193c7d42425b Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 09:57:43 +0100 Subject: [PATCH 54/62] Remove redundant `api_version_gt()` util --- hypothesis-python/src/hypothesis/extra/array_api.py | 13 ++++--------- hypothesis-python/tests/array_api/conftest.py | 3 +-- hypothesis-python/tests/array_api/test_arrays.py | 4 ++-- .../tests/array_api/test_scalar_dtypes.py | 5 ++--- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index a30c5be8f3..b8d2616fd1 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -65,18 +65,13 @@ ] -# Be sure to keep versions in ascending order so api_version_gt() works RELEASED_VERSIONS = ("2021.12",) -assert sorted(RELEASED_VERSIONS) == list(RELEASED_VERSIONS) # sanity check NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",) +assert sorted(NOMINAL_VERSIONS) == list(NOMINAL_VERSIONS) # sanity check NominalVersion = Literal["2021.12", "draft"] assert get_args(NominalVersion) == NOMINAL_VERSIONS # sanity check -def api_version_gt(api_version1: NominalVersion, api_version2: NominalVersion) -> bool: - return NOMINAL_VERSIONS.index(api_version1) > NOMINAL_VERSIONS.index(api_version2) - - INT_NAMES = ("int8", "int16", "int32", "int64") UINT_NAMES = ("uint8", "uint16", "uint32", "uint64") ALL_INT_NAMES = INT_NAMES + UINT_NAMES @@ -154,7 +149,7 @@ def find_castable_builtin_for_dtype( stubs.extend(int_stubs) stubs.extend(float_stubs) - if api_version_gt(api_version, "2021.12"): + if api_version > "2021.12": complex_dtypes, complex_stubs = partition_attributes_and_stubs( xp, COMPLEX_NAMES ) @@ -644,7 +639,7 @@ def _numeric_dtypes( ) -> st.SearchStrategy[DataType]: """Return a strategy for all numeric dtype objects.""" strat: st.SearchStrategy[DataType] = _real_dtypes(xp) - if api_version_gt(api_version, "2021.12"): + if api_version > "2021.12": strat |= _complex_dtypes(xp) return strat @@ -1065,7 +1060,7 @@ def __repr__(self): indices=indices, ) - if api_version_gt(api_version, "2021.12"): + if api_version > "2021.12": @defines_strategy() def complex_dtypes( diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 3be9059c5b..16765d93f8 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -20,7 +20,6 @@ from hypothesis.extra.array_api import ( NOMINAL_VERSIONS, NominalVersion, - api_version_gt, make_strategies_namespace, mock_xp, ) @@ -124,7 +123,7 @@ def pytest_collection_modifyitems(config, items): else: item.callspec.params["xps"].api_version min_version: NominalVersion = marker.args[0] - if api_version_gt(min_version, item.callspec.params["xps"].api_version): + if item.callspec.params["xps"].api_version < min_version: item.add_marker( pytest.mark.skip(reason=f"requires api_version=>{min_version}") ) diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py index 78eb33ea02..b36520688d 100644 --- a/hypothesis-python/tests/array_api/test_arrays.py +++ b/hypothesis-python/tests/array_api/test_arrays.py @@ -12,7 +12,7 @@ from hypothesis import given, strategies as st from hypothesis.errors import InvalidArgument -from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES, api_version_gt +from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES from hypothesis.internal.floats import width_smallest_normals from tests.array_api.common import ( @@ -98,7 +98,7 @@ def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name): def test_draw_arrays_from_dtype_name_strategies(xp, xps, data): """Draw arrays from dtype name strategies.""" all_names = ("bool",) + REAL_NAMES - if api_version_gt(xps.api_version, "2021.12"): + if xps.api_version > "2021.12": all_names += COMPLEX_NAMES sample_names = data.draw( st.lists(st.sampled_from(all_names), min_size=1, unique=True) diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py index 09831eadd1..09e294bd1d 100644 --- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py +++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py @@ -18,7 +18,6 @@ NUMERIC_NAMES, REAL_NAMES, UINT_NAMES, - api_version_gt, ) from tests.array_api.common import MIN_VER_FOR_COMPLEX @@ -48,7 +47,7 @@ def test_all_generated_dtypes_are_of_group(xp, xps, strat_name, dtype_names): def test_all_generated_scalar_dtypes_are_scalar(xp, xps): """Strategy only generates scalar dtypes.""" - if api_version_gt(xps.api_version, "2021.12"): + if xps.api_version > "2021.12": dtypes = [getattr(xp, n) for n in DTYPE_NAMES] else: dtypes = [getattr(xp, n) for n in ("bool",) + REAL_NAMES] @@ -57,7 +56,7 @@ def test_all_generated_scalar_dtypes_are_scalar(xp, xps): def test_all_generated_numeric_dtypes_are_numeric(xp, xps): """Strategy only generates numeric dtypes.""" - if api_version_gt(xps.api_version, "2021.12"): + if xps.api_version > "2021.12": dtypes = [getattr(xp, n) for n in NUMERIC_NAMES] else: dtypes = [getattr(xp, n) for n in REAL_NAMES] From 89ee94bd0ce0d841d4e5b8a1adb766854aa82d62 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:00:48 +0100 Subject: [PATCH 55/62] Move ini registration of `xp_min_version()` to top-level conftest Windows won't pick on it otherwise --- hypothesis-python/tests/array_api/conftest.py | 8 -------- hypothesis-python/tests/conftest.py | 4 ++++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index 16765d93f8..db026c7e45 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -105,14 +105,6 @@ def pytest_generate_tests(metafunc): metafunc.parametrize("xp", xp_params) -def pytest_configure(config): - config.addinivalue_line( - "markers", - "xp_min_version(api_version): " - "mark test to run when greater or equal to api_version", - ) - - def pytest_collection_modifyitems(config, items): for item in items: if all(f in item.fixturenames for f in ["xp", "xps"]): diff --git a/hypothesis-python/tests/conftest.py b/hypothesis-python/tests/conftest.py index bcbd024903..cb015988fb 100644 --- a/hypothesis-python/tests/conftest.py +++ b/hypothesis-python/tests/conftest.py @@ -40,6 +40,10 @@ def pytest_configure(config): config.addinivalue_line("markers", "slow: pandas expects this marker to exist.") + config.addinivalue_line( + "markers", + "xp_min_version(api_version): run when greater or equal to api_version", + ) def pytest_addoption(parser): From d0c353c77b4589d63986a72d9cc63db8ce0d1753 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:08:19 +0100 Subject: [PATCH 56/62] Clearer `xp_min_version()` logic in modifyitems hook --- hypothesis-python/tests/array_api/conftest.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py index db026c7e45..7ee6a1f4eb 100644 --- a/hypothesis-python/tests/array_api/conftest.py +++ b/hypothesis-python/tests/array_api/conftest.py @@ -107,15 +107,13 @@ def pytest_generate_tests(metafunc): def pytest_collection_modifyitems(config, items): for item in items: - if all(f in item.fixturenames for f in ["xp", "xps"]): - try: - marker = next(m for m in item.own_markers if m.name == "xp_min_version") - except StopIteration: - pass - else: - item.callspec.params["xps"].api_version - min_version: NominalVersion = marker.args[0] - if item.callspec.params["xps"].api_version < min_version: + if "xps" in item.fixturenames: + markers = [m for m in item.own_markers if m.name == "xp_min_version"] + if markers: + assert len(markers) == 1 # sanity check + min_version: NominalVersion = markers[0].args[0] + xps_version: NominalVersion = item.callspec.params["xps"].api_version + if xps_version < min_version: item.add_marker( pytest.mark.skip(reason=f"requires api_version=>{min_version}") ) From a5dd30518a12a2056f2490eefab84ce7d7c47bfb Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:11:04 +0100 Subject: [PATCH 57/62] Revert erroneous docstring change --- hypothesis-python/src/hypothesis/extra/array_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index b8d2616fd1..3c5c185781 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -201,7 +201,7 @@ def _from_dtype( :xp-ref:`valid dtype `. Compatible ``**kwargs`` are passed to the inferred strategy function for - integers, floats. This allows you to customise the min and max values, + integers and floats. This allows you to customise the min and max values, and exclude non-finite numbers. This is particularly useful when kwargs are passed through from :func:`arrays()`, as it seamlessly handles the ``width`` or other representable bounds for you. From 927b80a1ce2508ef7f96a569af9561d1e42d52a0 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:17:08 +0100 Subject: [PATCH 58/62] Include `xp` name in `complex_dtypes()` error --- hypothesis-python/src/hypothesis/extra/array_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 3c5c185781..fbe4e47bc1 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -1032,7 +1032,8 @@ def complex_dtypes(self): except KeyError as e: raise AttributeError( "You attempted to access 'complex_dtypes', but it is not " - f"available for api_version='{self.api_version}'." + f"available for api_version='{self.api_version}' of " + f"xp={self.name}." ) from e def __repr__(self): From 2a884fac161a69782a66f2d0b64709e67a0c738c Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:32:52 +0100 Subject: [PATCH 59/62] Sanity check for `MIN_VER_FOR_COMPLEX` --- hypothesis-python/tests/array_api/common.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py index a816a62cb0..cf706d970a 100644 --- a/hypothesis-python/tests/array_api/common.py +++ b/hypothesis-python/tests/array_api/common.py @@ -13,7 +13,12 @@ import pytest -from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES, NominalVersion +from hypothesis.extra.array_api import ( + COMPLEX_NAMES, + REAL_NAMES, + RELEASED_VERSIONS, + NominalVersion, +) from hypothesis.internal.floats import next_up __all__ = [ @@ -24,8 +29,10 @@ ] -# This should be updated for when a released version includes complex numbers +# This should be updated to the next spec release, which should include complex numbers MIN_VER_FOR_COMPLEX: NominalVersion = "draft" +if len(RELEASED_VERSIONS) > 1: + assert MIN_VER_FOR_COMPLEX == RELEASED_VERSIONS[1] def installed_array_modules() -> Dict[str, EntryPoint]: From 057d4de07706d2dc1bf58865efa93457e59291f2 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Tue, 27 Sep 2022 10:56:35 +0100 Subject: [PATCH 60/62] `make_strategies_namespace()` repr omits `api_version` when inferred --- .../src/hypothesis/extra/array_api.py | 22 ++++++++-------- .../tests/array_api/test_pretty.py | 25 +++++++++++++++---- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index fbe4e47bc1..39d1a509be 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -898,12 +898,6 @@ def make_strategies_namespace( "you're using the latest version of Hypothesis, then open an issue if " "one doesn't already exist." ) - check_argument( - api_version is None - or (isinstance(api_version, str) and api_version in NOMINAL_VERSIONS), - f"{api_version=}, but api_version must be None, or a valid version " - f"string {RELEASED_VERSIONS}. {not_available_msg}", - ) if api_version is None: check_argument( hasattr(xp, "__array_api_version__"), @@ -919,6 +913,14 @@ def make_strategies_namespace( f"be a valid version string {RELEASED_VERSIONS}. {not_available_msg}", ) api_version = xp.__array_api_version__ + inferred_version = True + else: + check_argument( + isinstance(api_version, str) and api_version in NOMINAL_VERSIONS, + f"{api_version=}, but api_version must be None, or a valid version " + f"string {RELEASED_VERSIONS}. {not_available_msg}", + ) + inferred_version = False try: array = xp.zeros(1) array.__array_namespace__() @@ -1037,10 +1039,10 @@ def complex_dtypes(self): ) from e def __repr__(self): - return ( - f"make_strategies_namespace(" - f"{self.name}, api_version='{self.api_version}')" - ) + f_args = self.name + if not inferred_version: + f_args += f", api_version='{self.api_version}'" + return f"make_strategies_namespace({f_args})" kwargs = dict( name=xp.__name__, diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py index 7b3127774d..bb38f6427f 100644 --- a/hypothesis-python/tests/array_api/test_pretty.py +++ b/hypothesis-python/tests/array_api/test_pretty.py @@ -12,6 +12,9 @@ import pytest +from hypothesis.errors import InvalidArgument +from hypothesis.extra.array_api import make_strategies_namespace + from tests.array_api.common import MIN_VER_FOR_COMPLEX @@ -80,10 +83,22 @@ def test_namespaced_strategies_repr(xp, xps, name, valid_args): assert xp.__name__ not in repr(strat), f"{xp.__name__} in strat repr" -def test_strategies_namespace_repr(xp, xps): - """Strategies namespace has good repr.""" - expected = ( - f"make_strategies_namespace({xp.__name__}, api_version='{xps.api_version}')" - ) +@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") +def test_inferred_version_strategies_namespace_repr(xp): + """Strategies namespace has good repr when api_version=None.""" + try: + xps = make_strategies_namespace(xp) + except InvalidArgument as e: + pytest.skip(str(e)) + expected = f"make_strategies_namespace({xp.__name__})" + assert repr(xps) == expected + assert str(xps) == expected + + +@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") +def test_specified_version_strategies_namespace_repr(xp): + """Strategies namespace has good repr when api_version is specified.""" + xps = make_strategies_namespace(xp, api_version="2021.12") + expected = f"make_strategies_namespace({xp.__name__}, api_version='2021.12')" assert repr(xps) == expected assert str(xps) == expected From bc4bcb6a308bb82e0d4a796f154987ddb38fa594 Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Wed, 28 Sep 2022 19:42:18 +0100 Subject: [PATCH 61/62] Distinct cache keys when `api_version=None` --- .../src/hypothesis/extra/array_api.py | 2 +- .../array_api/test_strategies_namespace.py | 53 +++++++++++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 39d1a509be..19b16b5a57 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -1076,7 +1076,7 @@ def complex_dtypes( namespace = StrategiesNamespace(**kwargs) try: - _args_to_xps[(xp, api_version)] = namespace + _args_to_xps[(xp, None if inferred_version else api_version)] = namespace except TypeError: pass diff --git a/hypothesis-python/tests/array_api/test_strategies_namespace.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py index 83bfeefbb0..cf7e453233 100644 --- a/hypothesis-python/tests/array_api/test_strategies_namespace.py +++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py @@ -21,20 +21,32 @@ ) from hypothesis.strategies import SearchStrategy +pytestmark = pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") -@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") -def test_caching(xp, monkeypatch): + +class HashableArrayModuleFactory: + """ + mock_xp cannot be hashed and thus cannot be used in our cache. So just for + the purposes of testing the cache, we wrap it with an unsafe hash method. + """ + + def __getattr__(self, name): + return getattr(mock_xp, name) + + def __hash__(self): + return hash(tuple(sorted(mock_xp.__dict__))) + + +@pytest.mark.parametrize("api_version", ["2021.12", None]) +def test_caching(api_version, monkeypatch): """Caches namespaces respective to arguments.""" - try: - hash(xp) - except TypeError: - pytest.skip("xp not hashable") - assert isinstance(array_api._args_to_xps, WeakValueDictionary) + xp = HashableArrayModuleFactory() + assert isinstance(array_api._args_to_xps, WeakValueDictionary) # sanity check monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary()) assert len(array_api._args_to_xps) == 0 # sanity check - xps1 = array_api.make_strategies_namespace(xp, api_version="2021.12") + xps1 = array_api.make_strategies_namespace(xp, api_version=api_version) assert len(array_api._args_to_xps) == 1 - xps2 = array_api.make_strategies_namespace(xp, api_version="2021.12") + xps2 = array_api.make_strategies_namespace(xp, api_version=api_version) assert len(array_api._args_to_xps) == 1 assert isinstance(xps2, SimpleNamespace) assert xps2 is xps1 @@ -43,7 +55,28 @@ def test_caching(xp, monkeypatch): assert len(array_api._args_to_xps) == 0 -@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning") +@pytest.mark.parametrize( + "api_version1, api_version2", [(None, "2021.12"), ("2021.12", None)] +) +def test_inferred_namespace_is_cached_seperately( + api_version1, api_version2, monkeypatch +): + """Results from inferred versions do not share the same cache key as results + from specified versions.""" + xp = HashableArrayModuleFactory() + xp.__array_api_version__ = "2021.12" + assert isinstance(array_api._args_to_xps, WeakValueDictionary) # sanity check + monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary()) + assert len(array_api._args_to_xps) == 0 # sanity check + xps1 = array_api.make_strategies_namespace(xp, api_version=api_version1) + assert xps1.api_version == "2021.12" # sanity check + assert len(array_api._args_to_xps) == 1 + xps2 = array_api.make_strategies_namespace(xp, api_version=api_version2) + assert xps2.api_version == "2021.12" # sanity check + assert len(array_api._args_to_xps) == 2 + assert xps2 is not xps1 + + def test_complex_dtypes_raises_on_2021_12(): """Accessing complex_dtypes() for 2021.12 strategy namespace raises helpful error, but accessing on future versions returns expected strategy.""" From 0e5155d6cf6bce07fcefc2323c2ed15b3833591e Mon Sep 17 00:00:00 2001 From: Matthew Barber Date: Thu, 29 Sep 2022 18:41:24 +0100 Subject: [PATCH 62/62] Inferred `api_version` shares cache key with specified versions --- .../src/hypothesis/extra/array_api.py | 16 ++++++++-------- .../tests/array_api/test_strategies_namespace.py | 10 ++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index 19b16b5a57..6cd05d47c4 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -886,13 +886,6 @@ def make_strategies_namespace( True """ - try: - namespace = _args_to_xps[(xp, api_version)] - except (KeyError, TypeError): - pass - else: - return namespace - not_available_msg = ( "If the standard version you want is not available, please ensure " "you're using the latest version of Hypothesis, then open an issue if " @@ -930,6 +923,13 @@ def make_strategies_namespace( HypothesisWarning, ) + try: + namespace = _args_to_xps[(xp, api_version)] + except (KeyError, TypeError): + pass + else: + return namespace + @defines_strategy(force_reusable_values=True) def from_dtype( dtype: Union[DataType, str], @@ -1076,7 +1076,7 @@ def complex_dtypes( namespace = StrategiesNamespace(**kwargs) try: - _args_to_xps[(xp, None if inferred_version else api_version)] = namespace + _args_to_xps[(xp, api_version)] = namespace except TypeError: pass diff --git a/hypothesis-python/tests/array_api/test_strategies_namespace.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py index cf7e453233..d1f18a6c25 100644 --- a/hypothesis-python/tests/array_api/test_strategies_namespace.py +++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py @@ -58,10 +58,8 @@ def test_caching(api_version, monkeypatch): @pytest.mark.parametrize( "api_version1, api_version2", [(None, "2021.12"), ("2021.12", None)] ) -def test_inferred_namespace_is_cached_seperately( - api_version1, api_version2, monkeypatch -): - """Results from inferred versions do not share the same cache key as results +def test_inferred_namespace_shares_cache(api_version1, api_version2, monkeypatch): + """Results from inferred versions share the same cache key as results from specified versions.""" xp = HashableArrayModuleFactory() xp.__array_api_version__ = "2021.12" @@ -73,8 +71,8 @@ def test_inferred_namespace_is_cached_seperately( assert len(array_api._args_to_xps) == 1 xps2 = array_api.make_strategies_namespace(xp, api_version=api_version2) assert xps2.api_version == "2021.12" # sanity check - assert len(array_api._args_to_xps) == 2 - assert xps2 is not xps1 + assert len(array_api._args_to_xps) == 1 + assert xps2 is xps1 def test_complex_dtypes_raises_on_2021_12():