diff --git a/.github/workflows/numpy.yml b/.github/workflows/numpy.yml index e4694fb0..262874e7 100644 --- a/.github/workflows/numpy.yml +++ b/.github/workflows/numpy.yml @@ -41,6 +41,12 @@ jobs: array_api_tests/test_signatures.py::test_function_keyword_only_args[__dlpack__] # floor_divide has an issue related to https://github.com/data-apis/array-api/issues/264 array_api_tests/test_elementwise_functions.py::test_floor_divide + # mesgrid doesn't return all arrays as the promoted dtype + array_api_tests/test_type_promotion.py::test_meshgrid + # https://github.com/numpy/numpy/pull/20066#issuecomment-947056094 + array_api_tests/test_type_promotion.py::test_where + # shape mismatches are not handled + array_api_tests/test_type_promotion.py::test_tensordot EOF diff --git a/array_api_tests/dtype_helpers.py b/array_api_tests/dtype_helpers.py index a60abef1..28844c87 100644 --- a/array_api_tests/dtype_helpers.py +++ b/array_api_tests/dtype_helpers.py @@ -1,8 +1,10 @@ from warnings import warn -from typing import NamedTuple +from functools import lru_cache +from typing import NamedTuple, Tuple, Union from . import _array_module as xp from ._array_module import _UndefinedStub +from .typing import DataType, ScalarType __all__ = [ @@ -28,19 +30,20 @@ 'binary_op_to_symbol', 'unary_op_to_symbol', 'inplace_op_to_symbol', + 'fmt_types', ] -_int_names = ('int8', 'int16', 'int32', 'int64') _uint_names = ('uint8', 'uint16', 'uint32', 'uint64') +_int_names = ('int8', 'int16', 'int32', 'int64') _float_names = ('float32', 'float64') -_dtype_names = ('bool',) + _int_names + _uint_names + _float_names +_dtype_names = ('bool',) + _uint_names + _int_names + _float_names -int_dtypes = tuple(getattr(xp, name) for name in _int_names) uint_dtypes = tuple(getattr(xp, name) for name in _uint_names) +int_dtypes = tuple(getattr(xp, name) for name in _int_names) float_dtypes = tuple(getattr(xp, name) for name in _float_names) -all_int_dtypes = int_dtypes + uint_dtypes +all_int_dtypes = uint_dtypes + int_dtypes numeric_dtypes = all_int_dtypes + float_dtypes all_dtypes = (xp.bool,) + numeric_dtypes bool_and_all_int_dtypes = (xp.bool,) + all_int_dtypes @@ -148,6 +151,17 @@ class MinMax(NamedTuple): } +def result_type(*dtypes: DataType): + if len(dtypes) == 0: + raise ValueError() + elif len(dtypes) == 1: + return dtypes[0] + result = promotion_table[dtypes[0], dtypes[1]] + for i in range(2, len(dtypes)): + result = promotion_table[result, dtypes[i]] + return result + + dtype_nbits = { **{d: 8 for d in [xp.int8, xp.uint8]}, **{d: 16 for d in [xp.int16, xp.uint16]}, @@ -163,6 +177,7 @@ class MinMax(NamedTuple): func_in_dtypes = { + # elementwise 'abs': numeric_dtypes, 'acos': float_dtypes, 'acosh': float_dtypes, @@ -219,10 +234,13 @@ class MinMax(NamedTuple): 'tan': float_dtypes, 'tanh': float_dtypes, 'trunc': numeric_dtypes, + # searching + 'where': all_dtypes, } func_returns_bool = { + # elementwise 'abs': False, 'acos': False, 'acosh': False, @@ -279,6 +297,8 @@ class MinMax(NamedTuple): 'tan': False, 'tanh': False, 'trunc': False, + # searching + 'where': False, } @@ -352,3 +372,15 @@ class MinMax(NamedTuple): inplace_op_to_symbol[iop] = f'{symbol}=' func_in_dtypes[iop] = func_in_dtypes[op] func_returns_bool[iop] = func_returns_bool[op] + + +@lru_cache +def fmt_types(types: Tuple[Union[DataType, ScalarType], ...]) -> str: + f_types = [] + for type_ in types: + try: + f_types.append(dtype_to_name[type_]) + except KeyError: + # i.e. dtype is bool, int, or float + f_types.append(type_.__name__) + return ', '.join(f_types) diff --git a/array_api_tests/hypothesis_helpers.py b/array_api_tests/hypothesis_helpers.py index 96328720..f6f0a8e2 100644 --- a/array_api_tests/hypothesis_helpers.py +++ b/array_api_tests/hypothesis_helpers.py @@ -2,16 +2,16 @@ from operator import mul from math import sqrt import itertools -from typing import Tuple +from typing import Tuple, Optional, List from hypothesis import assume from hypothesis.strategies import (lists, integers, sampled_from, shared, floats, just, composite, one_of, - none, booleans) -from hypothesis.strategies._internal.strategies import SearchStrategy + none, booleans, SearchStrategy) from .pytest_helpers import nargs from .array_helpers import ndindex +from .typing import DataType, Shape from . import dtype_helpers as dh from ._array_module import (full, float32, float64, bool as bool_dtype, _UndefinedStub, eye, broadcast_to) @@ -50,7 +50,7 @@ _dtype_categories = [(xp.bool,), dh.uint_dtypes, dh.int_dtypes, dh.float_dtypes] _sorted_dtypes = [d for category in _dtype_categories for d in category] -def _dtypes_sorter(dtype_pair): +def _dtypes_sorter(dtype_pair: Tuple[DataType, DataType]): dtype1, dtype2 = dtype_pair if dtype1 == dtype2: return _sorted_dtypes.index(dtype1) @@ -67,7 +67,7 @@ def _dtypes_sorter(dtype_pair): key += 1 return key -promotable_dtypes = sorted(dh.promotion_table.keys(), key=_dtypes_sorter) +promotable_dtypes: List[Tuple[DataType, DataType]] = sorted(dh.promotion_table.keys(), key=_dtypes_sorter) if FILTER_UNDEFINED_DTYPES: promotable_dtypes = [ @@ -77,10 +77,34 @@ def _dtypes_sorter(dtype_pair): ] -def mutually_promotable_dtypes(dtype_objs=dh.all_dtypes): - return sampled_from( - [(i, j) for i, j in promotable_dtypes if i in dtype_objs and j in dtype_objs] - ) +def mutually_promotable_dtypes( + max_size: Optional[int] = 2, + *, + dtypes: Tuple[DataType, ...] = dh.all_dtypes, +) -> SearchStrategy[Tuple[DataType, ...]]: + if max_size == 2: + return sampled_from( + [(i, j) for i, j in promotable_dtypes if i in dtypes and j in dtypes] + ) + if isinstance(max_size, int) and max_size < 2: + raise ValueError(f'{max_size=} should be >=2') + strats = [] + category_samples = { + category: [d for d in dtypes if d in category] for category in _dtype_categories + } + for samples in category_samples.values(): + if len(samples) > 0: + strat = lists(sampled_from(samples), min_size=2, max_size=max_size) + strats.append(strat) + if len(category_samples[dh.uint_dtypes]) > 0 and len(category_samples[dh.int_dtypes]) > 0: + mixed_samples = category_samples[dh.uint_dtypes] + category_samples[dh.int_dtypes] + strat = lists(sampled_from(mixed_samples), min_size=2, max_size=max_size) + if xp.uint64 in mixed_samples: + strat = strat.filter( + lambda l: not (xp.uint64 in l and any(d in dh.int_dtypes for d in l)) + ) + return one_of(strats).map(tuple) + # shared() allows us to draw either the function or the function name and they # will both correspond to the same function. @@ -113,15 +137,19 @@ def tuples(elements, *, min_size=0, max_size=None, unique_by=None, unique=False) # Use this to avoid memory errors with NumPy. # See https://github.com/numpy/numpy/issues/15753 -shapes = xps.array_shapes(min_dims=0, min_side=0).filter( - lambda shape: prod(i for i in shape if i) < MAX_ARRAY_SIZE -) +def shapes(**kw): + kw.setdefault('min_dims', 0) + kw.setdefault('min_side', 0) + return xps.array_shapes(**kw).filter( + lambda shape: prod(i for i in shape if i) < MAX_ARRAY_SIZE + ) + one_d_shapes = xps.array_shapes(min_dims=1, max_dims=1, min_side=0, max_side=SQRT_MAX_ARRAY_SIZE) # Matrix shapes assume stacks of matrices @composite -def matrix_shapes(draw, stack_shapes=shapes): +def matrix_shapes(draw, stack_shapes=shapes()): stack_shape = draw(stack_shapes) mat_shape = draw(xps.array_shapes(max_dims=2, min_dims=2)) shape = stack_shape + mat_shape @@ -135,9 +163,11 @@ def matrix_shapes(draw, stack_shapes=shapes): elements=dict(allow_nan=False, allow_infinity=False)) -def mutually_broadcastable_shapes(num_shapes: int) -> SearchStrategy[Tuple[Tuple]]: +def mutually_broadcastable_shapes( + num_shapes: int, **kw +) -> SearchStrategy[Tuple[Shape, ...]]: return ( - xps.mutually_broadcastable_shapes(num_shapes) + xps.mutually_broadcastable_shapes(num_shapes, **kw) .map(lambda BS: BS.input_shapes) .filter(lambda shapes: all( prod(i for i in s if i > 0) < MAX_ARRAY_SIZE for s in shapes @@ -164,13 +194,13 @@ def positive_definite_matrices(draw, dtypes=xps.floating_dtypes()): # using something like # https://github.com/scikit-learn/scikit-learn/blob/844b4be24/sklearn/datasets/_samples_generator.py#L1351. n = draw(integers(0)) - shape = draw(shapes) + (n, n) + shape = draw(shapes()) + (n, n) assume(prod(i for i in shape if i) < MAX_ARRAY_SIZE) dtype = draw(dtypes) return broadcast_to(eye(n, dtype=dtype), shape) @composite -def invertible_matrices(draw, dtypes=xps.floating_dtypes(), stack_shapes=shapes): +def invertible_matrices(draw, dtypes=xps.floating_dtypes(), stack_shapes=shapes()): # For now, just generate stacks of diagonal matrices. n = draw(integers(0, SQRT_MAX_ARRAY_SIZE),) stack_shape = draw(stack_shapes) @@ -318,9 +348,10 @@ def multiaxis_indices(draw, shapes): def two_mutual_arrays( - dtype_objs=dh.all_dtypes, two_shapes=two_mutually_broadcastable_shapes -): - mutual_dtypes = shared(mutually_promotable_dtypes(dtype_objs)) + dtypes: Tuple[DataType, ...] = dh.all_dtypes, + two_shapes: SearchStrategy[Tuple[Shape, Shape]] = two_mutually_broadcastable_shapes, +) -> SearchStrategy: + mutual_dtypes = shared(mutually_promotable_dtypes(dtypes=dtypes)) mutual_shapes = shared(two_shapes) arrays1 = xps.arrays( dtype=mutual_dtypes.map(lambda pair: pair[0]), diff --git a/array_api_tests/meta/test_hypothesis_helpers.py b/array_api_tests/meta/test_hypothesis_helpers.py index 93a63f8e..f583e711 100644 --- a/array_api_tests/meta/test_hypothesis_helpers.py +++ b/array_api_tests/meta/test_hypothesis_helpers.py @@ -9,12 +9,11 @@ from .. import dtype_helpers as dh from .. import hypothesis_helpers as hh from ..test_broadcasting import broadcast_shapes -from ..test_elementwise_functions import sanity_check UNDEFINED_DTYPES = any(isinstance(d, _UndefinedStub) for d in dh.all_dtypes) pytestmark = [pytest.mark.skipif(UNDEFINED_DTYPES, reason="undefined dtypes")] -@given(hh.mutually_promotable_dtypes([xp.float32, xp.float64])) +@given(hh.mutually_promotable_dtypes(dtypes=dh.float_dtypes)) def test_mutually_promotable_dtypes(pairs): assert pairs in ( (xp.float32, xp.float32), @@ -32,7 +31,7 @@ def valid_shape(shape) -> bool: ) -@given(hh.shapes) +@given(hh.shapes()) def test_shapes(shape): assert valid_shape(shape) @@ -52,7 +51,7 @@ def test_two_broadcastable_shapes(pair): @given(*hh.two_mutual_arrays()) def test_two_mutual_arrays(x1, x2): - sanity_check(x1, x2) + assert (x1.dtype, x2.dtype) in dh.promotion_table.keys() assert broadcast_shapes(x1.shape, x2.shape) in (x1.shape, x2.shape) diff --git a/array_api_tests/meta/test_pytest_helpers.py b/array_api_tests/meta/test_pytest_helpers.py new file mode 100644 index 00000000..9b0f4fad --- /dev/null +++ b/array_api_tests/meta/test_pytest_helpers.py @@ -0,0 +1,13 @@ +from pytest import raises + +from .. import pytest_helpers as ph +from .. import _array_module as xp + + +def test_assert_dtype(): + ph.assert_dtype("promoted_func", (xp.uint8, xp.int8), xp.int16) + with raises(AssertionError): + ph.assert_dtype("bad_func", (xp.uint8, xp.int8), xp.float32) + ph.assert_dtype("bool_func", (xp.uint8, xp.int8), xp.bool, xp.bool) + ph.assert_dtype("single_promoted_func", (xp.uint8,), xp.uint8) + ph.assert_dtype("single_bool_func", (xp.uint8,), xp.bool, xp.bool) diff --git a/array_api_tests/pytest_helpers.py b/array_api_tests/pytest_helpers.py index 354ad50e..129b6fc3 100644 --- a/array_api_tests/pytest_helpers.py +++ b/array_api_tests/pytest_helpers.py @@ -1,5 +1,10 @@ from inspect import getfullargspec +from typing import Optional, Tuple + +from . import dtype_helpers as dh from . import function_stubs +from .typing import DataType + def raises(exceptions, function, message=''): """ @@ -33,3 +38,25 @@ def doesnt_raise(function, message=''): def nargs(func_name): return len(getfullargspec(getattr(function_stubs, func_name)).args) + + +def assert_dtype( + func_name: str, + in_dtypes: Tuple[DataType, ...], + out_dtype: DataType, + expected: Optional[DataType] = None, + *, + out_name: str = "out.dtype", +): + f_in_dtypes = dh.fmt_types(in_dtypes) + f_out_dtype = dh.dtype_to_name[out_dtype] + if expected is None: + expected = dh.result_type(*in_dtypes) + f_expected = dh.dtype_to_name[expected] + msg = ( + f"{out_name}={f_out_dtype}, but should be {f_expected} " + f"[{func_name}({f_in_dtypes})]" + ) + assert out_dtype == expected, msg + + diff --git a/array_api_tests/test_broadcasting.py b/array_api_tests/test_broadcasting.py index 1a1c9c47..d951d48f 100644 --- a/array_api_tests/test_broadcasting.py +++ b/array_api_tests/test_broadcasting.py @@ -110,7 +110,7 @@ def test_broadcast_shapes_explicit_spec(): @pytest.mark.parametrize('func_name', [i for i in elementwise_functions.__all__ if nargs(i) > 1]) -@given(shape1=shapes, shape2=shapes, data=data()) +@given(shape1=shapes(), shape2=shapes(), data=data()) def test_broadcasting_hypothesis(func_name, shape1, shape2, data): # Internal consistency checks assert nargs(func_name) == 2 diff --git a/array_api_tests/test_creation_functions.py b/array_api_tests/test_creation_functions.py index 58575a8a..46587d68 100644 --- a/array_api_tests/test_creation_functions.py +++ b/array_api_tests/test_creation_functions.py @@ -70,7 +70,7 @@ 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(shapes, kwargs(dtype=none() | shared_dtypes)) +@given(shapes(), kwargs(dtype=none() | shared_dtypes)) def test_empty(shape, kw): out = empty(shape, **kw) dtype = kw.get("dtype", None) or xp.float64 @@ -84,7 +84,7 @@ def test_empty(shape, kw): @given( - x=xps.arrays(dtype=xps.scalar_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.scalar_dtypes(), shape=shapes()), kw=kwargs(dtype=none() | xps.scalar_dtypes()) ) def test_empty_like(x, kw): @@ -146,7 +146,7 @@ def full_fill_values(draw): @given( - shape=shapes, + shape=shapes(), fill_value=full_fill_values(), kw=shared(kwargs(dtype=none() | xps.scalar_dtypes()), key="full_kw"), ) @@ -184,7 +184,7 @@ def full_like_fill_values(draw): @given( - x=xps.arrays(dtype=shared_dtypes, shape=shapes), + 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"), ) @@ -255,7 +255,7 @@ def make_one(dtype): return True -@given(shapes, kwargs(dtype=none() | xps.scalar_dtypes())) +@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 @@ -268,7 +268,7 @@ def test_ones(shape, kw): @given( - x=xps.arrays(dtype=dtypes, shape=shapes), + x=xps.arrays(dtype=dtypes, shape=shapes()), kw=kwargs(dtype=none() | xps.scalar_dtypes()), ) def test_ones_like(x, kw): @@ -291,7 +291,7 @@ def make_zero(dtype): return False -@given(shapes, kwargs(dtype=none() | xps.scalar_dtypes())) +@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 @@ -304,7 +304,7 @@ def test_zeros(shape, kw): @given( - x=xps.arrays(dtype=dtypes, shape=shapes), + x=xps.arrays(dtype=dtypes, shape=shapes()), kw=kwargs(dtype=none() | xps.scalar_dtypes()), ) def test_zeros_like(x, kw): diff --git a/array_api_tests/test_elementwise_functions.py b/array_api_tests/test_elementwise_functions.py index ee714458..5aee5c2d 100644 --- a/array_api_tests/test_elementwise_functions.py +++ b/array_api_tests/test_elementwise_functions.py @@ -29,11 +29,11 @@ integer_or_boolean_scalars = hh.array_scalars(hh.integer_or_boolean_dtypes) boolean_scalars = hh.array_scalars(hh.boolean_dtypes) -two_integer_dtypes = hh.mutually_promotable_dtypes(dh.all_int_dtypes) -two_floating_dtypes = hh.mutually_promotable_dtypes(dh.float_dtypes) -two_numeric_dtypes = hh.mutually_promotable_dtypes(dh.numeric_dtypes) -two_integer_or_boolean_dtypes = hh.mutually_promotable_dtypes(dh.bool_and_all_int_dtypes) -two_boolean_dtypes = hh.mutually_promotable_dtypes((xp.bool,)) +two_integer_dtypes = hh.mutually_promotable_dtypes(dtypes=dh.all_int_dtypes) +two_floating_dtypes = hh.mutually_promotable_dtypes(dtypes=dh.float_dtypes) +two_numeric_dtypes = hh.mutually_promotable_dtypes(dtypes=dh.numeric_dtypes) +two_integer_or_boolean_dtypes = hh.mutually_promotable_dtypes(dtypes=dh.bool_and_all_int_dtypes) +two_boolean_dtypes = hh.mutually_promotable_dtypes(dtypes=(xp.bool,)) two_any_dtypes = hh.mutually_promotable_dtypes() @st.composite @@ -42,14 +42,7 @@ def two_array_scalars(draw, dtype1, dtype2): # hh.mutually_promotable_dtypes()) return draw(hh.array_scalars(st.just(dtype1))), draw(hh.array_scalars(st.just(dtype2))) -# TODO: refactor this into dtype_helpers.py, see https://github.com/data-apis/array-api-tests/pull/26 -def sanity_check(x1, x2): - try: - dh.promotion_table[x1.dtype, x2.dtype] - except ValueError: - raise RuntimeError("Error in test generation (probably a bug in the test suite") - -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_abs(x): if dh.is_int_dtype(x.dtype): minval = dh.dtype_ranges[x.dtype][0] @@ -66,7 +59,7 @@ def test_abs(x): # abs(x) = x for x >= 0 ah.assert_exactly_equal(a[ah.logical_not(less_zero)], x[ah.logical_not(less_zero)]) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_acos(x): a = xp.acos(x) ONE = ah.one(x.shape, x.dtype) @@ -80,7 +73,7 @@ def test_acos(x): # nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_acosh(x): a = xp.acosh(x) ONE = ah.one(x.shape, x.dtype) @@ -94,7 +87,6 @@ def test_acosh(x): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_add(x1, x2): - sanity_check(x1, x2) a = xp.add(x1, x2) b = xp.add(x2, x1) @@ -102,7 +94,7 @@ def test_add(x1, x2): ah.assert_exactly_equal(a, b) # TODO: Test that add is actually addition -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_asin(x): a = xp.asin(x) ONE = ah.one(x.shape, x.dtype) @@ -113,7 +105,7 @@ def test_asin(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_asinh(x): a = xp.asinh(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -123,7 +115,7 @@ def test_asinh(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_atan(x): a = xp.atan(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -136,7 +128,6 @@ def test_atan(x): @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_atan2(x1, x2): - sanity_check(x1, x2) a = xp.atan2(x1, x2) INFINITY1 = ah.infinity(x1.shape, x1.dtype) INFINITY2 = ah.infinity(x2.shape, x2.dtype) @@ -170,7 +161,7 @@ def test_atan2(x1, x2): ah.assert_exactly_equal(ah.logical_or(ah.logical_and(negx1, posx2), ah.logical_and(negx1, negx2)), nega) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_atanh(x): a = xp.atanh(x) ONE = ah.one(x.shape, x.dtype) @@ -183,7 +174,6 @@ def test_atanh(x): @given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) def test_bitwise_and(x1, x2): - sanity_check(x1, x2) out = xp.bitwise_and(x1, x2) # TODO: generate indices without broadcasting arrays (see test_equal comment) @@ -210,7 +200,6 @@ def test_bitwise_and(x1, x2): @given(*hh.two_mutual_arrays(dh.all_int_dtypes)) def test_bitwise_left_shift(x1, x2): - sanity_check(x1, x2) assume(not ah.any(ah.isnegative(x2))) out = xp.bitwise_left_shift(x1, x2) @@ -230,7 +219,7 @@ def test_bitwise_left_shift(x1, x2): vals_shift = ah.int_to_dtype(vals_shift, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) assert vals_shift == res -@given(xps.arrays(dtype=hh.integer_or_boolean_dtypes, shape=hh.shapes)) +@given(xps.arrays(dtype=hh.integer_or_boolean_dtypes, shape=hh.shapes())) def test_bitwise_invert(x): out = xp.bitwise_invert(x) # Compare against the Python ~ operator. @@ -249,7 +238,6 @@ def test_bitwise_invert(x): @given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) def test_bitwise_or(x1, x2): - sanity_check(x1, x2) out = xp.bitwise_or(x1, x2) # TODO: generate indices without broadcasting arrays (see test_equal comment) @@ -276,7 +264,6 @@ def test_bitwise_or(x1, x2): @given(*hh.two_mutual_arrays(dh.all_int_dtypes)) def test_bitwise_right_shift(x1, x2): - sanity_check(x1, x2) assume(not ah.any(ah.isnegative(x2))) out = xp.bitwise_right_shift(x1, x2) @@ -297,7 +284,6 @@ def test_bitwise_right_shift(x1, x2): @given(*hh.two_mutual_arrays(dh.bool_and_all_int_dtypes)) def test_bitwise_xor(x1, x2): - sanity_check(x1, x2) out = xp.bitwise_xor(x1, x2) # TODO: generate indices without broadcasting arrays (see test_equal comment) @@ -322,7 +308,7 @@ def test_bitwise_xor(x1, x2): vals_xor = ah.int_to_dtype(vals_xor, dh.dtype_nbits[out.dtype], dh.dtype_signed[out.dtype]) assert vals_xor == res -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_ceil(x): # This test is almost identical to test_floor() a = xp.ceil(x) @@ -333,7 +319,7 @@ def test_ceil(x): integers = ah.isintegral(x) ah.assert_exactly_equal(a[integers], x[integers]) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_cos(x): a = xp.cos(x) ONE = ah.one(x.shape, x.dtype) @@ -344,7 +330,7 @@ def test_cos(x): # to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_cosh(x): a = xp.cosh(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -356,7 +342,6 @@ def test_cosh(x): @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_divide(x1, x2): - sanity_check(x1, x2) xp.divide(x1, x2) # There isn't much we can test here. The spec doesn't require any behavior # beyond the special cases, and indeed, there aren't many mathematical @@ -367,7 +352,6 @@ def test_divide(x1, x2): @given(*hh.two_mutual_arrays()) def test_equal(x1, x2): - sanity_check(x1, x2) a = ah.equal(x1, x2) # NOTE: ah.assert_exactly_equal() itself uses ah.equal(), so we must be careful # not to use it here. Otherwise, the test would be circular and @@ -379,7 +363,7 @@ def test_equal(x1, x2): # First we broadcast the arrays so that they can be indexed uniformly. # TODO: it should be possible to skip this step if we instead generate - # indices to x1 and x2 that correspond to the broadcasted hh.shapes. This + # indices to x1 and x2 that correspond to the broadcasted shapes. This # would avoid the dependence in this test on broadcast_to(). shape = broadcast_shapes(x1.shape, x2.shape) _x1 = xp.broadcast_to(x1, shape) @@ -414,7 +398,7 @@ def test_equal(x1, x2): assert aidx.shape == x1idx.shape == x2idx.shape assert bool(aidx) == (scalar_func(x1idx) == scalar_func(x2idx)) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_exp(x): a = xp.exp(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -425,7 +409,7 @@ def test_exp(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_expm1(x): a = xp.expm1(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -436,7 +420,7 @@ def test_expm1(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_floor(x): # This test is almost identical to test_ceil a = xp.floor(x) @@ -449,7 +433,6 @@ def test_floor(x): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_floor_divide(x1, x2): - sanity_check(x1, x2) if dh.is_int_dtype(x1.dtype): # The spec does not specify the behavior for division by 0 for integer # dtypes. A library may choose to raise an exception in this case, so @@ -473,7 +456,6 @@ def test_floor_divide(x1, x2): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_greater(x1, x2): - sanity_check(x1, x2) a = xp.greater(x1, x2) # See the comments in test_equal() for a description of how this test @@ -502,7 +484,6 @@ def test_greater(x1, x2): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_greater_equal(x1, x2): - sanity_check(x1, x2) a = xp.greater_equal(x1, x2) # See the comments in test_equal() for a description of how this test @@ -529,7 +510,7 @@ def test_greater_equal(x1, x2): assert aidx.shape == x1idx.shape == x2idx.shape assert bool(aidx) == (scalar_func(x1idx) >= scalar_func(x2idx)) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isfinite(x): a = ah.isfinite(x) TRUE = ah.true(x.shape) @@ -545,7 +526,7 @@ def test_isfinite(x): s = float(x[idx]) assert bool(a[idx]) == math.isfinite(s) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isinf(x): a = xp.isinf(x) FALSE = ah.false(x.shape) @@ -560,7 +541,7 @@ def test_isinf(x): s = float(x[idx]) assert bool(a[idx]) == math.isinf(s) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_isnan(x): a = ah.isnan(x) FALSE = ah.false(x.shape) @@ -577,7 +558,6 @@ def test_isnan(x): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_less(x1, x2): - sanity_check(x1, x2) a = ah.less(x1, x2) # See the comments in test_equal() for a description of how this test @@ -606,7 +586,6 @@ def test_less(x1, x2): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_less_equal(x1, x2): - sanity_check(x1, x2) a = ah.less_equal(x1, x2) # See the comments in test_equal() for a description of how this test @@ -633,7 +612,7 @@ def test_less_equal(x1, x2): assert aidx.shape == x1idx.shape == x2idx.shape assert bool(aidx) == (scalar_func(x1idx) <= scalar_func(x2idx)) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log(x): a = xp.log(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -644,7 +623,7 @@ def test_log(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log1p(x): a = xp.log1p(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -655,7 +634,7 @@ def test_log1p(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log2(x): a = xp.log2(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -666,7 +645,7 @@ def test_log2(x): # mapped to nan, which is already tested in the special cases. ah.assert_exactly_equal(domain, codomain) -@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.floating_dtypes(), shape=hh.shapes())) def test_log10(x): a = xp.log10(x) INFINITY = ah.infinity(x.shape, x.dtype) @@ -679,7 +658,6 @@ def test_log10(x): @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_logaddexp(x1, x2): - sanity_check(x1, x2) xp.logaddexp(x1, x2) # The spec doesn't require any behavior for this function. We could test # that this is indeed an approximation of log(exp(x1) + exp(x2)), but we @@ -687,7 +665,6 @@ def test_logaddexp(x1, x2): @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_and(x1, x2): - sanity_check(x1, x2) a = ah.logical_and(x1, x2) # See the comments in test_equal @@ -698,7 +675,7 @@ def test_logical_and(x1, x2): for idx in ah.ndindex(shape): assert a[idx] == (bool(_x1[idx]) and bool(_x2[idx])) -@given(xps.arrays(dtype=xp.bool, shape=hh.shapes)) +@given(xps.arrays(dtype=xp.bool, shape=hh.shapes())) def test_logical_not(x): a = ah.logical_not(x) @@ -707,7 +684,6 @@ def test_logical_not(x): @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_or(x1, x2): - sanity_check(x1, x2) a = ah.logical_or(x1, x2) # See the comments in test_equal @@ -720,7 +696,6 @@ def test_logical_or(x1, x2): @given(*hh.two_mutual_arrays([xp.bool])) def test_logical_xor(x1, x2): - sanity_check(x1, x2) a = xp.logical_xor(x1, x2) # See the comments in test_equal @@ -733,14 +708,13 @@ def test_logical_xor(x1, x2): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_multiply(x1, x2): - sanity_check(x1, x2) a = xp.multiply(x1, x2) b = xp.multiply(x2, x1) # multiply is commutative ah.assert_exactly_equal(a, b) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_negative(x): out = ah.negative(x) @@ -762,7 +736,6 @@ def test_negative(x): @given(*hh.two_mutual_arrays()) def test_not_equal(x1, x2): - sanity_check(x1, x2) a = xp.not_equal(x1, x2) # See the comments in test_equal() for a description of how this test @@ -790,7 +763,7 @@ def test_not_equal(x1, x2): assert bool(aidx) == (scalar_func(x1idx) != scalar_func(x2idx)) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_positive(x): out = xp.positive(x) # Positive does nothing @@ -798,7 +771,6 @@ def test_positive(x): @given(*hh.two_mutual_arrays(dh.float_dtypes)) def test_pow(x1, x2): - sanity_check(x1, x2) xp.pow(x1, x2) # There isn't much we can test here. The spec doesn't require any behavior # beyond the special cases, and indeed, there aren't many mathematical @@ -809,7 +781,6 @@ def test_pow(x1, x2): @given(*hh.two_mutual_arrays(dh.numeric_dtypes)) def test_remainder(x1, x2): assume(len(x1.shape) <= len(x2.shape)) # TODO: rework same sign testing below to remove this - sanity_check(x1, x2) out = xp.remainder(x1, x2) # out and x2 should have the same sign. @@ -817,7 +788,7 @@ def test_remainder(x1, x2): not_nan = ah.logical_not(ah.logical_or(ah.isnan(out), ah.isnan(x2))) ah.assert_same_sign(out[not_nan], x2[not_nan]) -@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes)) +@given(xps.arrays(dtype=xps.numeric_dtypes(), shape=hh.shapes())) def test_round(x): a = xp.round(x) @@ -868,7 +839,6 @@ def test_sqrt(x): @given(two_numeric_dtypes.flatmap(lambda i: two_array_scalars(*i))) def test_subtract(args): x1, x2 = args - sanity_check(x1, x2) # a = xp.subtract(x1, x2) @given(floating_scalars) diff --git a/array_api_tests/test_indexing.py b/array_api_tests/test_indexing.py index 54467633..9d3a4a88 100644 --- a/array_api_tests/test_indexing.py +++ b/array_api_tests/test_indexing.py @@ -58,8 +58,8 @@ def test_slicing(size, s): for i in range(len(sliced_list)): assert sliced_array[i] == sliced_list[i], "Slice index did not give the same elements as slicing an equivalent Python list" -@given(shared(shapes, key='array_shapes'), - multiaxis_indices(shapes=shared(shapes, key='array_shapes'))) +@given(shared(shapes(), key='array_shapes'), + multiaxis_indices(shapes=shared(shapes(), key='array_shapes'))) def test_multiaxis_indexing(shape, idx): # NOTE: Out of bounds indices (both integer and slices) are out of scope # for the spec. If you get a (valid) out of bounds error, it indicates a diff --git a/array_api_tests/test_linalg.py b/array_api_tests/test_linalg.py index 421eeacd..a180a516 100644 --- a/array_api_tests/test_linalg.py +++ b/array_api_tests/test_linalg.py @@ -26,8 +26,8 @@ mutually_promotable_dtypes, one_d_shapes, two_mutually_broadcastable_shapes, SQRT_MAX_ARRAY_SIZE, finite_matrices) -from .pytest_helpers import raises from . import dtype_helpers as dh +from . import pytest_helpers as ph from .test_broadcasting import broadcast_shapes @@ -105,7 +105,7 @@ def cross_args(draw, dtype_objects=dh.numeric_dtypes): in the drawn axis. """ - shape = list(draw(shapes)) + shape = list(draw(shapes())) size = len(shape) assume(size > 0) @@ -113,7 +113,7 @@ def cross_args(draw, dtype_objects=dh.numeric_dtypes): axis = kw.get('axis', -1) shape[axis] = 3 - mutual_dtypes = shared(mutually_promotable_dtypes(dtype_objects)) + mutual_dtypes = shared(mutually_promotable_dtypes(dtypes=dtype_objects)) arrays1 = xps.arrays( dtype=mutual_dtypes.map(lambda pair: pair[0]), shape=shape, @@ -274,13 +274,13 @@ def test_matmul(x1, x2): or len(x1.shape) >= 2 and len(x2.shape) >= 2 and x1.shape[-1] != x2.shape[-2]): # The spec doesn't specify what kind of exception is used here. Most # libraries will use a custom exception class. - raises(Exception, lambda: _array_module.matmul(x1, x2), + ph.raises(Exception, lambda: _array_module.matmul(x1, x2), "matmul did not raise an exception for invalid shapes") return else: res = _array_module.matmul(x1, x2) - assert res.dtype == dh.promotion_table[x1.dtype, x2.dtype], "matmul() did not return the correct dtype" + ph.assert_dtype("matmul", (x1.dtype, x2.dtype), res.dtype) if len(x1.shape) == len(x2.shape) == 1: assert res.shape == () @@ -297,7 +297,7 @@ def test_matmul(x1, x2): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(axis=todo, keepdims=todo, ord=todo) ) def test_matrix_norm(x, kw): @@ -329,7 +329,7 @@ def test_matrix_power(x, n): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(rtol=todo) ) def test_matrix_rank(x, kw): @@ -355,7 +355,7 @@ def test_matrix_transpose(x): @pytest.mark.xp_extension('linalg') @given( - *two_mutual_arrays(dtype_objs=dh.numeric_dtypes, + *two_mutual_arrays(dtypes=dh.numeric_dtypes, two_shapes=tuples(one_d_shapes, one_d_shapes)) ) def test_outer(x1, x2): @@ -379,7 +379,7 @@ def test_outer(x1, x2): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(rtol=todo) ) def test_pinv(x, kw): @@ -523,15 +523,15 @@ def test_svd(x, kw): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), ) def test_svdvals(x): # res = linalg.svdvals(x) pass @given( - x1=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), - x2=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x1=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), + x2=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(axes=todo) ) def test_tensordot(x1, x2, kw): @@ -540,7 +540,7 @@ def test_tensordot(x1, x2, kw): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(offset=todo) ) def test_trace(x, kw): @@ -548,8 +548,8 @@ def test_trace(x, kw): pass @given( - x1=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), - x2=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x1=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), + x2=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(axis=todo) ) def test_vecdot(x1, x2, kw): @@ -558,7 +558,7 @@ def test_vecdot(x1, x2, kw): @pytest.mark.xp_extension('linalg') @given( - x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes), + x=xps.arrays(dtype=xps.floating_dtypes(), shape=shapes()), kw=kwargs(axis=todo, keepdims=todo, ord=todo) ) def test_vector_norm(x, kw): diff --git a/array_api_tests/test_type_promotion.py b/array_api_tests/test_type_promotion.py index ea4e6eb8..e3c37c11 100644 --- a/array_api_tests/test_type_promotion.py +++ b/array_api_tests/test_type_promotion.py @@ -2,19 +2,72 @@ https://data-apis.github.io/array-api/latest/API_specification/type_promotion.html """ from collections import defaultdict -from typing import Tuple, Type, Union, List +from typing import Tuple, Union, List import pytest from hypothesis import assume, given, reject from hypothesis import strategies as st from . import _array_module as xp -from . import array_helpers as ah from . import dtype_helpers as dh from . import hypothesis_helpers as hh +from . import pytest_helpers as ph from . import xps +from .typing import DataType, ScalarType, Param from .function_stubs import elementwise_functions -from .pytest_helpers import nargs + + +# TODO: move tests not covering elementwise funcs/ops into standalone tests +# result_type, meshgrid, concat, stack, where, tensordor, vecdot + + +@given(hh.mutually_promotable_dtypes(None)) +def test_result_type(dtypes): + out = xp.result_type(*dtypes) + ph.assert_dtype('result_type', dtypes, out, out_name='out') + + +@given( + dtypes=hh.mutually_promotable_dtypes(None, dtypes=dh.numeric_dtypes), + data=st.data(), +) +def test_meshgrid(dtypes, data): + arrays = [] + shapes = data.draw(hh.mutually_broadcastable_shapes(len(dtypes)), label='shapes') + for i, (dtype, shape) in enumerate(zip(dtypes, shapes), 1): + x = data.draw(xps.arrays(dtype=dtype, shape=shape), label=f'x{i}') + arrays.append(x) + out = xp.meshgrid(*arrays) + for i, x in enumerate(out): + ph.assert_dtype('meshgrid', dtypes, x.dtype, out_name=f'out[{i}].dtype') + + +@given( + shape=hh.shapes(min_dims=1), + dtypes=hh.mutually_promotable_dtypes(None, dtypes=dh.numeric_dtypes), + data=st.data(), +) +def test_concat(shape, dtypes, data): + arrays = [] + for i, dtype in enumerate(dtypes, 1): + x = data.draw(xps.arrays(dtype=dtype, shape=shape), label=f'x{i}') + arrays.append(x) + out = xp.concat(arrays) + ph.assert_dtype('concat', dtypes, out.dtype) + + +@given( + shape=hh.shapes(), + dtypes=hh.mutually_promotable_dtypes(None), + data=st.data(), +) +def test_stack(shape, dtypes, data): + arrays = [] + for i, dtype in enumerate(dtypes, 1): + x = data.draw(xps.arrays(dtype=dtype, shape=shape), label=f'x{i}') + arrays.append(x) + out = xp.stack(arrays) + ph.assert_dtype('stack', dtypes, out.dtype) bitwise_shift_funcs = [ @@ -27,37 +80,27 @@ ] -DT = Type -ScalarType = Union[Type[bool], Type[int], Type[float]] - - -# We apply filters to xps.arrays() so we don't generate array elements that -# are erroneous or undefined for a function/operator. -filters = defaultdict( - lambda: lambda _: True, - {func: lambda x: ah.all(x > 0) for func in bitwise_shift_funcs}, +# We pass kwargs to the elements strategy used by xps.arrays() so that we don't +# generate array elements that are erroneous or undefined for a function. +func_elements = defaultdict( + lambda: None, {func: {'min_value': 1} for func in bitwise_shift_funcs} ) def make_id( - func_name: str, in_dtypes: Tuple[Union[DT, ScalarType], ...], out_dtype: DT + func_name: str, + in_dtypes: Tuple[Union[DataType, ScalarType], ...], + out_dtype: DataType, ) -> str: - f_in_dtypes = [] - for dtype in in_dtypes: - try: - f_in_dtypes.append(dh.dtype_to_name[dtype]) - except KeyError: - # i.e. dtype is bool, int, or float - f_in_dtypes.append(dtype.__name__) - f_args = ', '.join(f_in_dtypes) + f_args = dh.fmt_types(in_dtypes) f_out_dtype = dh.dtype_to_name[out_dtype] return f'{func_name}({f_args}) -> {f_out_dtype}' -func_params: List[Tuple[str, Tuple[DT, ...], DT]] = [] +func_params: List[Param[str, Tuple[DataType, ...], DataType]] = [] for func_name in elementwise_functions.__all__: valid_in_dtypes = dh.func_in_dtypes[func_name] - ndtypes = nargs(func_name) + ndtypes = ph.nargs(func_name) if ndtypes == 1: for in_dtype in valid_in_dtypes: out_dtype = xp.bool if dh.func_returns_bool[func_name] else in_dtype @@ -89,10 +132,11 @@ def make_id( @given(data=st.data()) def test_func_promotion(func_name, in_dtypes, out_dtype, data): func = getattr(xp, func_name) - x_filter = filters[func_name] + elements = func_elements[func_name] if len(in_dtypes) == 1: x = data.draw( - xps.arrays(dtype=in_dtypes[0], shape=hh.shapes).filter(x_filter), label='x' + xps.arrays(dtype=in_dtypes[0], shape=hh.shapes(), elements=elements), + label='x', ) out = func(x) else: @@ -102,23 +146,64 @@ def test_func_promotion(func_name, in_dtypes, out_dtype, data): ) for i, (dtype, shape) in enumerate(zip(in_dtypes, shapes), 1): x = data.draw( - xps.arrays(dtype=dtype, shape=shape).filter(x_filter), label=f'x{i}' + xps.arrays(dtype=dtype, shape=shape, elements=elements), label=f'x{i}' ) arrays.append(x) try: out = func(*arrays) except OverflowError: reject() - assert out.dtype == out_dtype, f'{out.dtype=!s}, but should be {out_dtype}' + ph.assert_dtype(func_name, in_dtypes, out.dtype, out_dtype) + + +promotion_params: List[Param[Tuple[DataType, DataType], DataType]] = [] +for (dtype1, dtype2), promoted_dtype in dh.promotion_table.items(): + p = pytest.param( + (dtype1, dtype2), + promoted_dtype, + id=make_id('', (dtype1, dtype2), promoted_dtype), + ) + promotion_params.append(p) + + +@pytest.mark.parametrize('in_dtypes, out_dtype', promotion_params) +@given(shapes=hh.mutually_broadcastable_shapes(3), data=st.data()) +def test_where(in_dtypes, out_dtype, shapes, data): + x1 = data.draw(xps.arrays(dtype=in_dtypes[0], shape=shapes[0]), label='x1') + x2 = data.draw(xps.arrays(dtype=in_dtypes[1], shape=shapes[1]), label='x2') + cond = data.draw(xps.arrays(dtype=xp.bool, shape=shapes[2]), label='condition') + out = xp.where(cond, x1, x2) + ph.assert_dtype('where', in_dtypes, out.dtype, out_dtype) + + +numeric_promotion_params = promotion_params[1:] -op_params: List[Tuple[str, str, Tuple[DT, ...], DT]] = [] +@pytest.mark.parametrize('in_dtypes, out_dtype', numeric_promotion_params) +@given(shapes=hh.mutually_broadcastable_shapes(2, min_dims=2), data=st.data()) +def test_tensordot(in_dtypes, out_dtype, shapes, data): + x1 = data.draw(xps.arrays(dtype=in_dtypes[0], shape=shapes[0]), label='x1') + x2 = data.draw(xps.arrays(dtype=in_dtypes[1], shape=shapes[1]), label='x2') + out = xp.tensordot(x1, x2) + ph.assert_dtype('tensordot', in_dtypes, out.dtype, out_dtype) + + +@pytest.mark.parametrize('in_dtypes, out_dtype', numeric_promotion_params) +@given(shapes=hh.mutually_broadcastable_shapes(2, min_dims=1), data=st.data()) +def test_vecdot(in_dtypes, out_dtype, shapes, data): + x1 = data.draw(xps.arrays(dtype=in_dtypes[0], shape=shapes[0]), label='x1') + x2 = data.draw(xps.arrays(dtype=in_dtypes[1], shape=shapes[1]), label='x2') + out = xp.vecdot(x1, x2) + ph.assert_dtype('vecdot', in_dtypes, out.dtype, out_dtype) + + +op_params: List[Param[str, str, Tuple[DataType, ...], DataType]] = [] op_to_symbol = {**dh.unary_op_to_symbol, **dh.binary_op_to_symbol} for op, symbol in op_to_symbol.items(): if op == '__matmul__': continue valid_in_dtypes = dh.func_in_dtypes[op] - ndtypes = nargs(op) + ndtypes = ph.nargs(op) if ndtypes == 1: for in_dtype in valid_in_dtypes: out_dtype = xp.bool if dh.func_returns_bool[op] else in_dtype @@ -157,10 +242,11 @@ def test_func_promotion(func_name, in_dtypes, out_dtype, data): @pytest.mark.parametrize('op, expr, in_dtypes, out_dtype', op_params) @given(data=st.data()) def test_op_promotion(op, expr, in_dtypes, out_dtype, data): - x_filter = filters[op] + elements = func_elements[func_name] if len(in_dtypes) == 1: x = data.draw( - xps.arrays(dtype=in_dtypes[0], shape=hh.shapes).filter(x_filter), label='x' + xps.arrays(dtype=in_dtypes[0], shape=hh.shapes(), elements=elements), + label='x', ) out = eval(expr, {'x': x}) else: @@ -170,16 +256,16 @@ def test_op_promotion(op, expr, in_dtypes, out_dtype, data): ) for i, (dtype, shape) in enumerate(zip(in_dtypes, shapes), 1): locals_[f'x{i}'] = data.draw( - xps.arrays(dtype=dtype, shape=shape).filter(x_filter), label=f'x{i}' + xps.arrays(dtype=dtype, shape=shape, elements=elements), label=f'x{i}' ) try: out = eval(expr, locals_) except OverflowError: reject() - assert out.dtype == out_dtype, f'{out.dtype=!s}, but should be {out_dtype}' + ph.assert_dtype(op, in_dtypes, out.dtype, out_dtype) -inplace_params: List[Tuple[str, str, Tuple[DT, ...], DT]] = [] +inplace_params: List[Param[str, str, Tuple[DataType, ...], DataType]] = [] for op, symbol in dh.inplace_op_to_symbol.items(): if op == '__imatmul__': continue @@ -204,12 +290,12 @@ def test_op_promotion(op, expr, in_dtypes, out_dtype, data): @given(shapes=hh.mutually_broadcastable_shapes(2), data=st.data()) def test_inplace_op_promotion(op, expr, in_dtypes, out_dtype, shapes, data): assume(len(shapes[0]) >= len(shapes[1])) - x_filter = filters[op] + elements = func_elements[func_name] x1 = data.draw( - xps.arrays(dtype=in_dtypes[0], shape=shapes[0]).filter(x_filter), label='x1' + xps.arrays(dtype=in_dtypes[0], shape=shapes[0], elements=elements), label='x1' ) x2 = data.draw( - xps.arrays(dtype=in_dtypes[1], shape=shapes[1]).filter(x_filter), label='x2' + xps.arrays(dtype=in_dtypes[1], shape=shapes[1], elements=elements), label='x2' ) locals_ = {'x1': x1, 'x2': x2} try: @@ -217,10 +303,10 @@ def test_inplace_op_promotion(op, expr, in_dtypes, out_dtype, shapes, data): except OverflowError: reject() x1 = locals_['x1'] - assert x1.dtype == out_dtype, f'{x1.dtype=!s}, but should be {out_dtype}' + ph.assert_dtype(op, in_dtypes, x1.dtype, out_dtype, out_name='x1.dtype') -op_scalar_params: List[Tuple[str, str, DT, ScalarType, DT]] = [] +op_scalar_params: List[Param[str, str, DataType, ScalarType, DataType]] = [] for op, symbol in dh.binary_op_to_symbol.items(): if op == '__matmul__': continue @@ -241,20 +327,20 @@ def test_inplace_op_promotion(op, expr, in_dtypes, out_dtype, shapes, data): @pytest.mark.parametrize('op, expr, in_dtype, in_stype, out_dtype', op_scalar_params) @given(data=st.data()) def test_op_scalar_promotion(op, expr, in_dtype, in_stype, out_dtype, data): - x_filter = filters[op] + elements = func_elements[func_name] kw = {k: in_stype is float for k in ('allow_nan', 'allow_infinity')} s = data.draw(xps.from_dtype(in_dtype, **kw).map(in_stype), label='scalar') x = data.draw( - xps.arrays(dtype=in_dtype, shape=hh.shapes).filter(x_filter), label='x' + xps.arrays(dtype=in_dtype, shape=hh.shapes(), elements=elements), label='x' ) try: out = eval(expr, {'x': x, 's': s}) except OverflowError: reject() - assert out.dtype == out_dtype, f'{out.dtype=!s}, but should be {out_dtype}' + ph.assert_dtype(op, (in_dtype, in_stype), out.dtype, out_dtype) -inplace_scalar_params: List[Tuple[str, str, DT, ScalarType]] = [] +inplace_scalar_params: List[Param[str, str, DataType, ScalarType]] = [] for op, symbol in dh.inplace_op_to_symbol.items(): if op == '__imatmul__': continue @@ -273,10 +359,12 @@ def test_op_scalar_promotion(op, expr, in_dtype, in_stype, out_dtype, data): @pytest.mark.parametrize('op, expr, dtype, in_stype', inplace_scalar_params) @given(data=st.data()) def test_inplace_op_scalar_promotion(op, expr, dtype, in_stype, data): - x_filter = filters[op] + elements = func_elements[func_name] kw = {k: in_stype is float for k in ('allow_nan', 'allow_infinity')} s = data.draw(xps.from_dtype(dtype, **kw).map(in_stype), label='scalar') - x = data.draw(xps.arrays(dtype=dtype, shape=hh.shapes).filter(x_filter), label='x') + x = data.draw( + xps.arrays(dtype=dtype, shape=hh.shapes(), elements=elements), label='x' + ) locals_ = {'x': x, 's': s} try: exec(expr, locals_) @@ -284,6 +372,7 @@ def test_inplace_op_scalar_promotion(op, expr, dtype, in_stype, data): reject() x = locals_['x'] assert x.dtype == dtype, f'{x.dtype=!s}, but should be {dtype}' + ph.assert_dtype(op, (dtype, in_stype), x.dtype, dtype, out_name='x.dtype') if __name__ == '__main__': diff --git a/array_api_tests/typing.py b/array_api_tests/typing.py new file mode 100644 index 00000000..93165c72 --- /dev/null +++ b/array_api_tests/typing.py @@ -0,0 +1,13 @@ +from typing import Tuple, Type, Union, Any + +__all__ = [ + "DataType", + "ScalarType", + "Shape", + "Param", +] + +DataType = Type[Any] +ScalarType = Union[Type[bool], Type[int], Type[float]] +Shape = Tuple[int, ...] +Param = Tuple diff --git a/conftest.py b/conftest.py index 804bab29..64c46df7 100644 --- a/conftest.py +++ b/conftest.py @@ -102,5 +102,6 @@ def pytest_collection_modifyitems(config, items): except StopIteration: pass # workflow xfail_ids - if item.nodeid in xfail_ids: - item.add_marker(mark.xfail(reason='xfails.txt')) + for id_ in xfail_ids: + if item.nodeid.startswith(id_): + item.add_marker(mark.xfail(reason='xfails.txt'))