diff --git a/.github/workflows/numpy.yml b/.github/workflows/numpy.yml index 20111d49..3a0e0325 100644 --- a/.github/workflows/numpy.yml +++ b/.github/workflows/numpy.yml @@ -51,7 +51,6 @@ jobs: "array_api_tests/test_signatures.py::test_function_positional_args[__index__]", "array_api_tests/test_signatures.py::test_function_keyword_only_args[prod]", "array_api_tests/test_signatures.py::test_function_keyword_only_args[sum]", - ) def pytest_collection_modifyitems(config, items): diff --git a/array_api_tests/hypothesis_helpers.py b/array_api_tests/hypothesis_helpers.py index f0983b0f..91e9767a 100644 --- a/array_api_tests/hypothesis_helpers.py +++ b/array_api_tests/hypothesis_helpers.py @@ -2,11 +2,11 @@ from operator import mul from math import sqrt -from hypothesis.strategies import (lists, integers, builds, sampled_from, +from hypothesis import assume +from hypothesis.strategies import (lists, integers, sampled_from, shared, floats, just, composite, one_of, none, booleans) -from hypothesis.extra.numpy import mutually_broadcastable_shapes -from hypothesis import assume +from hypothesis.extra.array_api import make_strategies_namespace from .pytest_helpers import nargs from .array_helpers import (dtype_ranges, integer_dtype_objects, @@ -15,10 +15,14 @@ integer_or_boolean_dtype_objects, dtype_objects) from ._array_module import full, float32, float64, bool as bool_dtype, _UndefinedStub from . import _array_module +from . import _array_module as xp from .function_stubs import elementwise_functions +xps = make_strategies_namespace(xp) + + # Set this to True to not fail tests just because a dtype isn't implemented. # If no compatible dtype is implemented for a given test, the test will fail # with a hypothesis health check error. Note that this functionality will not @@ -42,8 +46,13 @@ boolean_dtypes = boolean_dtypes.filter(lambda x: not isinstance(x, _UndefinedStub)) dtypes = dtypes.filter(lambda x: not isinstance(x, _UndefinedStub)) -shared_dtypes = shared(dtypes) +shared_dtypes = shared(dtypes, key="dtype") +# TODO: Importing things from test_type_promotion should be replaced by +# something that won't cause a circular import. Right now we use @st.composite +# only because it returns a lazy-evaluated strategy - in the future this method +# should remove the composite wrapper, just returning sampled_from(dtype_pairs) +# instead of drawing from it. @composite def mutually_promotable_dtypes(draw, dtype_objects=dtype_objects): from .test_type_promotion import dtype_mapping, promotion_table @@ -55,17 +64,20 @@ def mutually_promotable_dtypes(draw, dtype_objects=dtype_objects): # pairs (XXX: Can we redesign the strategies so that they can prefer # shrinking dtypes over values?) sorted_table = sorted(promotion_table) - sorted_table = sorted(sorted_table, key=lambda ij: -1 if ij[0] == ij[1] else sorted_table.index(ij)) - dtype_pairs = [(dtype_mapping[i], dtype_mapping[j]) for i, j in - sorted_table] - - filtered_dtype_pairs = [(i, j) for i, j in dtype_pairs if i in - dtype_objects and j in dtype_objects] + sorted_table = sorted( + sorted_table, key=lambda ij: -1 if ij[0] == ij[1] else sorted_table.index(ij) + ) + dtype_pairs = [(dtype_mapping[i], dtype_mapping[j]) for i, j in sorted_table] if FILTER_UNDEFINED_DTYPES: - filtered_dtype_pairs = [(i, j) for i, j in filtered_dtype_pairs - if not isinstance(i, _UndefinedStub) - and not isinstance(j, _UndefinedStub)] - return draw(sampled_from(filtered_dtype_pairs)) + dtype_pairs = [(i, j) for i, j in dtype_pairs + if not isinstance(i, _UndefinedStub) + and not isinstance(j, _UndefinedStub)] + dtype_pairs = [(i, j) for i, j in dtype_pairs if i in dtype_objects and j in dtype_objects] + return draw(sampled_from(dtype_pairs)) + +shared_mutually_promotable_dtype_pairs = shared( + mutually_promotable_dtypes(), key="mutually_promotable_dtype_pair" +) # shared() allows us to draw either the function or the function name and they # will both correspond to the same function. @@ -96,36 +108,35 @@ def tuples(elements, *, min_size=0, max_size=None, unique_by=None, unique=False) return lists(elements, min_size=min_size, max_size=max_size, unique_by=unique_by, unique=unique).map(tuple) -shapes = tuples(integers(0, 10)).filter(lambda shape: prod(shape) < MAX_ARRAY_SIZE) - # Use this to avoid memory errors with NumPy. # See https://github.com/numpy/numpy/issues/15753 -shapes = tuples(integers(0, 10)).filter( - lambda shape: prod([i for i in shape if i]) < MAX_ARRAY_SIZE) +shapes = xps.array_shapes(min_dims=0, min_side=0).filter( + lambda shape: prod(i for i in shape if i) < MAX_ARRAY_SIZE +) -two_mutually_broadcastable_shapes = mutually_broadcastable_shapes(num_shapes=2)\ +two_mutually_broadcastable_shapes = xps.mutually_broadcastable_shapes(num_shapes=2)\ .map(lambda S: S.input_shapes)\ - .filter(lambda S: all(prod([i for i in shape if i]) < MAX_ARRAY_SIZE for shape in S)) + .filter(lambda S: all(prod(i for i in shape if i) < MAX_ARRAY_SIZE for shape in S)) @composite -def two_broadcastable_shapes(draw, shapes=shapes): +def two_broadcastable_shapes(draw): """ This will produce two shapes (shape1, shape2) such that shape2 can be broadcast to shape1. - """ from .test_broadcasting import broadcast_shapes - - shape1, shape2 = draw(two_mutually_broadcastable_shapes) - if broadcast_shapes(shape1, shape2) != shape1: - assume(False) + shape1, shape2 = draw(two_mutually_broadcastable_shapes) + assume(broadcast_shapes(shape1, shape2) == shape1) return (shape1, shape2) sizes = integers(0, MAX_ARRAY_SIZE) sqrt_sizes = integers(0, SQRT_MAX_ARRAY_SIZE) # TODO: Generate general arrays here, rather than just scalars. -numeric_arrays = builds(full, just((1,)), floats()) +numeric_arrays = xps.arrays( + dtype=shared(xps.floating_dtypes(), key='dtypes'), + shape=shared(xps.array_shapes(), key='shapes'), +) @composite def scalars(draw, dtypes, finite=False): @@ -230,3 +241,22 @@ def multiaxis_indices(draw, shapes): extra = draw(lists(one_of(integer_indices(sizes), slices(sizes)), min_size=0, max_size=3)) res += extra return tuple(res) + + +shared_arrays1 = xps.arrays( + dtype=shared_mutually_promotable_dtype_pairs.map(lambda pair: pair[0]), + shape=shared(two_mutually_broadcastable_shapes, key="shape_pair").map(lambda pair: pair[0]), +) +shared_arrays2 = xps.arrays( + dtype=shared_mutually_promotable_dtype_pairs.map(lambda pair: pair[1]), + shape=shared(two_mutually_broadcastable_shapes, key="shape_pair").map(lambda pair: pair[1]), +) + + +@composite +def kwargs(draw, **kw): + result = {} + for k, strat in kw.items(): + if draw(booleans()): + result[k] = draw(strat) + return result diff --git a/array_api_tests/meta_tests/test_hypothesis_helpers.py b/array_api_tests/meta_tests/test_hypothesis_helpers.py new file mode 100644 index 00000000..3a396a59 --- /dev/null +++ b/array_api_tests/meta_tests/test_hypothesis_helpers.py @@ -0,0 +1,72 @@ +from math import prod + +import pytest +from hypothesis import given, strategies as st + +from .. import _array_module as xp +from .._array_module import _UndefinedStub +from .. import array_helpers as ah +from .. import hypothesis_helpers as hh + +UNDEFINED_DTYPES = any(isinstance(d, _UndefinedStub) for d in ah.dtype_objects) +pytestmark = [pytest.mark.skipif(UNDEFINED_DTYPES, reason="undefined dtypes")] + + +@given(hh.mutually_promotable_dtypes([xp.float32, xp.float64])) +def test_mutually_promotable_dtypes(pairs): + assert pairs in ( + (xp.float32, xp.float32), + (xp.float32, xp.float64), + (xp.float64, xp.float32), + (xp.float64, xp.float64), + ) + + +def valid_shape(shape) -> bool: + return ( + all(isinstance(side, int) for side in shape) + and all(side >= 0 for side in shape) + and prod(shape) < hh.MAX_ARRAY_SIZE + ) + + +@given(hh.shapes) +def test_shapes(shape): + assert valid_shape(shape) + + +@given(hh.two_mutually_broadcastable_shapes) +def test_two_mutually_broadcastable_shapes(pair): + for shape in pair: + assert valid_shape(shape) + + +@given(hh.two_broadcastable_shapes()) +def test_two_broadcastable_shapes(pair): + for shape in pair: + assert valid_shape(shape) + + from ..test_broadcasting import broadcast_shapes + + assert broadcast_shapes(pair[0], pair[1]) == pair[0] + + +def test_kwargs(): + results = [] + + @given(hh.kwargs(n=st.integers(0, 10), c=st.from_regex("[a-f]"))) + def run(kw): + results.append(kw) + + run() + assert all(isinstance(kw, dict) for kw in results) + for size in [0, 1, 2]: + assert any(len(kw) == size for kw in results) + + n_results = [kw for kw in results if "n" in kw] + assert len(n_results) > 0 + assert all(isinstance(kw["n"], int) for kw in n_results) + + c_results = [kw for kw in results if "c" in kw] + assert len(c_results) > 0 + assert all(isinstance(kw["c"], str) for kw in c_results) diff --git a/array_api_tests/test_creation_functions.py b/array_api_tests/test_creation_functions.py index 5bd2b35b..b4566902 100644 --- a/array_api_tests/test_creation_functions.py +++ b/array_api_tests/test_creation_functions.py @@ -1,13 +1,18 @@ -from ._array_module import (asarray, arange, ceil, empty, eye, full, -equal, all, linspace, ones, zeros, isnan) +import math + +from ._array_module import (asarray, arange, ceil, empty, empty_like, eye, full, + full_like, equal, all, linspace, ones, ones_like, + zeros, zeros_like, isnan) +from . import _array_module as xp from .array_helpers import (is_integer_dtype, dtype_ranges, assert_exactly_equal, isintegral, is_float_dtype) from .hypothesis_helpers import (numeric_dtypes, dtypes, MAX_ARRAY_SIZE, shapes, sizes, sqrt_sizes, shared_dtypes, - scalars) + scalars, xps, kwargs) from hypothesis import assume, given -from hypothesis.strategies import integers, floats, one_of, none, booleans, just +from hypothesis.strategies import integers, floats, one_of, none, booleans, just, shared, composite + int_range = integers(-MAX_ARRAY_SIZE, MAX_ARRAY_SIZE) float_range = floats(-MAX_ARRAY_SIZE, MAX_ARRAY_SIZE, @@ -64,22 +69,32 @@ def test_arange(start, stop, step, dtype): or step < 0 and stop <= start)): assert a.size == ceil(asarray((stop-start)/step)), "arange() produced an array of the incorrect size" -@given(one_of(shapes, sizes), one_of(none(), dtypes)) -def test_empty(shape, dtype): - if dtype is None: - a = empty(shape) - assert is_float_dtype(a.dtype), "empty() should produce an array with the default floating point dtype" +@given(shapes, kwargs(dtype=none() | shared_dtypes)) +def test_empty(shape, kw): + out = empty(shape, **kw) + dtype = kw.get("dtype", None) or xp.float64 + if kw.get("dtype", None) is None: + assert is_float_dtype(out.dtype), f"empty() returned an array with dtype {out.dtype}, but should be the default float dtype" else: - a = empty(shape, dtype=dtype) - assert a.dtype == dtype - + assert out.dtype == dtype, f"{dtype=!s}, but empty() returned an array with dtype {out.dtype}" if isinstance(shape, int): shape = (shape,) - assert a.shape == shape, "empty() produced an array with an incorrect shape" + assert out.shape == shape, f"{shape=}, but empty() returned an array with shape {out.shape}" + + +@given( + x=xps.arrays(dtype=xps.scalar_dtypes(), shape=shapes), + kw=kwargs(dtype=none() | xps.scalar_dtypes()) +) +def test_empty_like(x, kw): + out = empty_like(x, **kw) + dtype = kw.get("dtype", None) or x.dtype + if kw.get("dtype", None) is None: + assert out.dtype == x.dtype, f"{x.dtype=!s}, but empty_like() returned an array with dtype {out.dtype}" + else: + assert out.dtype == dtype, f"{dtype=!s}, but empty_like() returned an array with dtype {out.dtype}" + assert out.shape == x.shape, f"{x.shape=}, but empty_like() returned an array with shape {out.shape}" -# TODO: implement empty_like (requires hypothesis arrays support) -def test_empty_like(): - pass # TODO: Use this method for all optional arguments optional_marker = object() @@ -94,7 +109,7 @@ def test_eye(n_rows, n_cols, k, dtype): else: a = eye(n_rows, n_cols, **kwargs) if dtype is None: - assert is_float_dtype(a.dtype), "eye() should produce an array with the default floating point dtype" + assert is_float_dtype(a.dtype), "eye() should return an array with the default floating point dtype" else: assert a.dtype == dtype, "eye() did not produce the correct dtype" @@ -111,28 +126,70 @@ def test_eye(n_rows, n_cols, k, dtype): else: assert a[i, j] == 0, "eye() did not produce a 0 off the diagonal" -@given(shapes, scalars(shared_dtypes), one_of(none(), shared_dtypes)) -def test_full(shape, fill_value, dtype): - kwargs = {} if dtype is None else {'dtype': dtype} - - a = full(shape, fill_value, **kwargs) - if dtype is None: - # TODO: Should it actually match the fill_value? - # assert a.dtype in _floating_dtypes, "eye() should produce an array with the default floating point dtype" - pass +@composite +def full_fill_values(draw): + kw = draw(shared(kwargs(dtype=none() | xps.scalar_dtypes()), key="full_kw")) + dtype = kw.get("dtype", None) or draw(xps.scalar_dtypes()) + return draw(xps.from_dtype(dtype)) + + +@given( + shape=shapes, + fill_value=full_fill_values(), + kw=shared(kwargs(dtype=none() | xps.scalar_dtypes()), key="full_kw"), +) +def test_full(shape, fill_value, kw): + out = full(shape, fill_value, **kw) + if kw.get("dtype", None): + dtype = kw["dtype"] + elif isinstance(fill_value, bool): + dtype = xp.bool + elif isinstance(fill_value, int): + dtype = xp.int64 else: - assert a.dtype == dtype - - assert a.shape == shape, "full() produced an array with incorrect shape" - if is_float_dtype(a.dtype) and isnan(asarray(fill_value)): - assert all(isnan(a)), "full() array did not equal the fill value" + dtype = xp.float64 + if kw.get("dtype", None) is None: + if dtype == xp.float64: + assert is_float_dtype(out.dtype), f"full() returned an array with dtype {out.dtype}, but should be the default float dtype" + elif dtype == xp.int64: + assert out.dtype == xp.int32 or out.dtype == xp.int64, f"full() returned an array with dtype {out.dtype}, but should be the default integer dtype" + else: + assert out.dtype == xp.bool, f"full() returned an array with dtype {out.dtype}, but should be the bool dtype" + else: + assert out.dtype == dtype + assert out.shape == shape, f"{shape=}, but full() returned an array with shape {out.shape}" + if is_float_dtype(out.dtype) and math.isnan(fill_value): + assert all(isnan(out)), "full() array did not equal the fill value" + else: + assert all(equal(out, asarray(fill_value, dtype=dtype))), "full() array did not equal the fill value" + + +@composite +def full_like_fill_values(draw): + kw = draw(shared(kwargs(dtype=none() | xps.scalar_dtypes()), key="full_like_kw")) + dtype = kw.get("dtype", None) or draw(shared_dtypes) + return draw(xps.from_dtype(dtype)) + + +@given( + x=xps.arrays(dtype=shared_dtypes, shape=shapes), + fill_value=full_like_fill_values(), + kw=shared(kwargs(dtype=none() | xps.scalar_dtypes()), key="full_like_kw"), +) +def test_full_like(x, fill_value, kw): + out = full_like(x, fill_value, **kw) + dtype = kw.get("dtype", None) or x.dtype + if kw.get("dtype", None) is None: + assert out.dtype == x.dtype, f"{x.dtype=!s}, but full_like() returned an array with dtype {out.dtype}" else: - assert all(equal(a, asarray(fill_value, **kwargs))), "full() array did not equal the fill value" + assert out.dtype == dtype, f"{dtype=!s}, but full_like() returned an array with dtype {out.dtype}" + assert out.shape == x.shape, "{x.shape=}, but full_like() returned an array with shape {out.shape}" + if is_float_dtype(dtype) and math.isnan(fill_value): + assert all(isnan(out)), "full_like() array did not equal the fill value" + else: + assert all(equal(out, asarray(fill_value, dtype=dtype))), "full_like() array did not equal the fill value" -# TODO: implement full_like (requires hypothesis arrays support) -def test_full_like(): - pass @given(scalars(shared_dtypes, finite=True), scalars(shared_dtypes, finite=True), @@ -152,11 +209,11 @@ def test_linspace(start, stop, num, dtype, endpoint): a = linspace(start, stop, num, **kwargs) if dtype is None: - assert is_float_dtype(a.dtype), "linspace() should produce an array with the default floating point dtype" + assert is_float_dtype(a.dtype), "linspace() should return an array with the default floating point dtype" else: assert a.dtype == dtype, "linspace() did not produce the correct dtype" - assert a.shape == (num,), "linspace() did not produce an array with the correct shape" + assert a.shape == (num,), "linspace() did not return an array with the correct shape" if endpoint in [None, True]: if num > 1: @@ -177,54 +234,75 @@ def test_linspace(start, stop, num, dtype, endpoint): # for i in range(1, num): # assert all(equal(a[i], full((), i*(stop - start)/n + start, dtype=dtype))), f"linspace() produced an array with an incorrect value at index {i}" -@given(shapes, one_of(none(), dtypes)) -def test_ones(shape, dtype): - kwargs = {} if dtype is None else {'dtype': dtype} - if dtype is None or is_float_dtype(dtype): - ONE = 1.0 + +def make_one(dtype): + if kwargs is None or is_float_dtype(dtype): + return 1.0 elif is_integer_dtype(dtype): - ONE = 1 + return 1 else: - ONE = True + return True - a = ones(shape, **kwargs) - if dtype is None: - # TODO: Should it actually match the fill_value? - # assert a.dtype in _floating_dtypes, "eye() should produce an array with the default floating point dtype" - pass +@given(shapes, kwargs(dtype=none() | xps.scalar_dtypes())) +def test_ones(shape, kw): + out = ones(shape, **kw) + dtype = kw.get("dtype", None) or xp.float64 + if kw.get("dtype", None) is None: + assert is_float_dtype(out.dtype), f"ones() returned an array with dtype {out.dtype}, but should be the default float dtype" else: - assert a.dtype == dtype - - assert a.shape == shape, "ones() produced an array with incorrect shape" - assert all(equal(a, full((), ONE, **kwargs))), "ones() array did not equal 1" + assert out.dtype == dtype, f"{dtype=!s}, but ones() returned an array with dtype {out.dtype}" + assert out.shape == shape, f"{shape=}, but empty() returned an array with shape {out.shape}" + assert all(equal(out, full((), make_one(dtype), dtype=dtype))), "ones() array did not equal 1" + + +@given( + x=xps.arrays(dtype=dtypes, shape=shapes), + kw=kwargs(dtype=none() | xps.scalar_dtypes()), +) +def test_ones_like(x, kw): + out = ones_like(x, **kw) + dtype = kw.get("dtype", None) or x.dtype + if kw.get("dtype", None) is None: + assert out.dtype == x.dtype, f"{x.dtype=!s}, but ones_like() returned an array with dtype {out.dtype}" + else: + assert out.dtype == dtype, f"{dtype=!s}, but ones_like() returned an array with dtype {out.dtype}" + assert out.shape == x.shape, "{x.shape=}, but ones_like() returned an array with shape {out.shape}" + assert all(equal(out, full((), make_one(dtype), dtype=dtype))), "ones_like() array elements did not equal 1" -# TODO: implement ones_like (requires hypothesis arrays support) -def test_ones_like(): - pass -@given(shapes, one_of(none(), dtypes)) -def test_zeros(shape, dtype): - kwargs = {} if dtype is None else {'dtype': dtype} - if dtype is None or is_float_dtype(dtype): - ZERO = 0.0 +def make_zero(dtype): + if is_float_dtype(dtype): + return 0.0 elif is_integer_dtype(dtype): - ZERO = 0 + return 0 else: - ZERO = False + return False - a = zeros(shape, **kwargs) - if dtype is None: - # TODO: Should it actually match the fill_value? - # assert a.dtype in _floating_dtypes, "eye() should produce an array with the default floating point dtype" - pass +@given(shapes, kwargs(dtype=none() | xps.scalar_dtypes())) +def test_zeros(shape, kw): + out = zeros(shape, **kw) + dtype = kw.get("dtype", None) or xp.float64 + if kw.get("dtype", None) is None: + assert is_float_dtype(out.dtype), "zeros() returned an array with dtype {out.dtype}, but should be the default float dtype" else: - assert a.dtype == dtype - - assert a.shape == shape, "zeros() produced an array with incorrect shape" - assert all(equal(a, full((), ZERO, **kwargs))), "zeros() array did not equal 0" + assert out.dtype == dtype, f"{dtype=!s}, but zeros() returned an array with dtype {out.dtype}" + assert out.shape == shape, "zeros() produced an array with incorrect shape" + assert all(equal(out, full((), make_zero(dtype), dtype=dtype))), "zeros() array did not equal 0" + + +@given( + x=xps.arrays(dtype=dtypes, shape=shapes), + kw=kwargs(dtype=none() | xps.scalar_dtypes()), +) +def test_zeros_like(x, kw): + out = zeros_like(x, **kw) + dtype = kw.get("dtype", None) or x.dtype + if kw.get("dtype", None) is None: + assert out.dtype == x.dtype, f"{x.dtype=!s}, but zeros_like() returned an array with dtype {out.dtype}" + else: + assert out.dtype == dtype, f"{dtype=!s}, but zeros_like() returned an array with dtype {out.dtype}" + assert out.shape == x.shape, "{x.shape=}, but zeros_like() returned an array with shape {out.shape}" + assert all(equal(out, full((), make_zero(dtype), dtype=out.dtype))), "zeros_like() array elements did not all equal 0" -# TODO: implement zeros_like (requires hypothesis arrays support) -def test_zeros_like(): - pass diff --git a/array_api_tests/test_elementwise_functions.py b/array_api_tests/test_elementwise_functions.py index a0afc040..01db8882 100644 --- a/array_api_tests/test_elementwise_functions.py +++ b/array_api_tests/test_elementwise_functions.py @@ -26,7 +26,7 @@ boolean_dtype_objects, floating_dtypes, numeric_dtypes, integer_or_boolean_dtypes, boolean_dtypes, mutually_promotable_dtypes, - array_scalars) + array_scalars, shared_arrays1, shared_arrays2) from .array_helpers import (assert_exactly_equal, negative, positive_mathematical_sign, negative_mathematical_sign, logical_not, @@ -374,9 +374,9 @@ def test_divide(args): # could test that this does implement IEEE 754 division, but we don't yet # have those sorts in general for this module. -@given(two_any_dtypes.flatmap(lambda i: two_array_scalars(*i))) -def test_equal(args): - x1, x2 = args + +@given(shared_arrays1, shared_arrays2) +def test_equal(x1, x2): sanity_check(x1, x2) a = _array_module.equal(x1, x2) # NOTE: assert_exactly_equal() itself uses equal(), so we must be careful diff --git a/requirements.txt b/requirements.txt index f22f1570..59fa4e81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ pytest -hypothesis +hypothesis>=6.21.5 regex removestar