diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..567d891614 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,4 @@ +RELEASE_TYPE: patch + +This patch adds an error for when ``shapes`` in :func:`xps.arrays()` is not +passed as either a valid shape or strategy. diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py index c94e306efb..b29703d239 100644 --- a/hypothesis-python/src/hypothesis/extra/array_api.py +++ b/hypothesis-python/src/hypothesis/extra/array_api.py @@ -468,16 +468,17 @@ def _arrays( return dtype.flatmap( lambda d: _arrays(xp, 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) ) - - if isinstance(dtype, str): - dtype = dtype_from_name(xp, dtype) - - if isinstance(shape, int): + elif isinstance(shape, int): shape = (shape,) + elif not isinstance(shape, tuple): + raise InvalidArgument(f"shape={shape} is not a valid shape or strategy") check_argument( all(isinstance(x, int) and x >= 0 for x in shape), f"shape={shape!r}, but all dimensions must be non-negative integers.", @@ -584,7 +585,6 @@ def _unsigned_integer_dtypes( ``sizes`` contains the unsigned integer sizes in bits, defaulting to ``(8, 16, 32, 64)`` which covers all valid sizes. """ - if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("int", sizes, (8, 16, 32, 64)) @@ -605,7 +605,6 @@ def _floating_dtypes( ``sizes`` contains the floating-point sizes in bits, defaulting to ``(32, 64)`` which covers all valid sizes. """ - if isinstance(sizes, int): sizes = (sizes,) check_valid_sizes("int", sizes, (32, 64)) @@ -662,7 +661,7 @@ def indices( allow_ellipsis: bool = True, ) -> st.SearchStrategy[BasicIndex]: """Return a strategy for :xp-ref:`valid indices ` of - arrays with the specified shape. + arrays with the specified shape, which may include dimensions of size zero. It generates tuples containing some mix of integers, :obj:`python:slice` objects, and ``...`` (an ``Ellipsis``). When a length-one tuple would be diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py index 7afbd961ec..9ffbde890c 100644 --- a/hypothesis-python/tests/array_api/test_argument_validation.py +++ b/hypothesis-python/tests/array_api/test_argument_validation.py @@ -33,6 +33,7 @@ def e(a, **kwargs): e(xps.arrays, dtype=xp.int8, shape=(0.5,)), e(xps.arrays, dtype=xp.int8, shape=1, fill=3), e(xps.arrays, dtype=xp.int8, shape=1, elements="not a strategy"), + e(xps.arrays, dtype=xp.int8, shape="not a shape or strategy"), e(xps.array_shapes, min_side=2, max_side=1), e(xps.array_shapes, min_dims=3, max_dims=2), e(xps.array_shapes, min_dims=-1), diff --git a/hypothesis-python/tests/array_api/test_from_dtype.py b/hypothesis-python/tests/array_api/test_from_dtype.py index 998e3e5885..709a954033 100644 --- a/hypothesis-python/tests/array_api/test_from_dtype.py +++ b/hypothesis-python/tests/array_api/test_from_dtype.py @@ -101,4 +101,4 @@ def test_from_dtype_with_kwargs(data, dtype, kwargs, predicate): def test_can_minimize_floats(): """Inferred float strategy minimizes to a good example.""" smallest = minimal(xps.from_dtype(xp.float32), lambda n: n >= 1.0) - assert smallest in (1, 50) + assert smallest == 1 diff --git a/hypothesis-python/tests/array_api/test_indices.py b/hypothesis-python/tests/array_api/test_indices.py index 0ca1269d51..87760d8158 100644 --- a/hypothesis-python/tests/array_api/test_indices.py +++ b/hypothesis-python/tests/array_api/test_indices.py @@ -21,32 +21,41 @@ from tests.common.debug import find_any -def test_indices_options(): - indexers = ( +def test_generate_indices_with_and_without_ellipsis(): + """Strategy can generate indices with and without Ellipsis.""" + strat = ( xps.array_shapes(min_dims=1, max_dims=32) .flatmap(xps.indices) .map(lambda idx: idx if isinstance(idx, tuple) else (idx,)) ) - find_any(indexers, lambda ix: Ellipsis in ix) - find_any(indexers, lambda ix: Ellipsis not in ix) + find_any(strat, lambda ix: Ellipsis in ix) + find_any(strat, lambda ix: Ellipsis not in ix) -def test_indices_can_generate_empty_tuple(): +def test_generate_empty_tuple(): + """Strategy generates empty tuples as indices.""" find_any(xps.indices(shape=(0, 0), allow_ellipsis=True), lambda ix: ix == ()) -def test_indices_can_generate_non_tuples(): +def test_generate_non_tuples(): + """Strategy generates non-tuples as indices.""" find_any( xps.indices(shape=(0, 0), allow_ellipsis=True), lambda ix: not isinstance(ix, tuple), ) -def test_indices_can_generate_long_ellipsis(): - # Runs of slice(None) - such as [0,:,:,:,0] - can be replaced by e.g. [0,...,0] +def test_generate_long_ellipsis(): + """Strategy can replace runs of slice(None) with Ellipsis. + + We specifically test if [0,...,0] is generated alongside [0,:,:,:,0] + """ + strat = xps.indices(shape=(1, 0, 0, 0, 1), max_dims=3, allow_ellipsis=True) + find_any(strat, lambda ix: len(ix) == 3 and ix[1] == Ellipsis) find_any( - xps.indices(shape=(1, 0, 0, 0, 1), max_dims=3, allow_ellipsis=True), - lambda ix: len(ix) == 3 and ix[1] == Ellipsis, + strat, + lambda ix: len(ix) == 5 + and all(isinstance(key, slice) and key == slice(None) for key in ix[1:3]), ) @@ -74,6 +83,7 @@ def test_indices_effeciently_generate_indexers(_): data=st.data(), ) def test_indices_generate_valid_indexers(shape, allow_ellipsis, data): + """Strategy generates valid indices.""" min_dims = data.draw(st.integers(0, len(shape)), label="min_dims") max_dims = data.draw( st.none() | st.integers(min_dims, len(shape)), label="max_dims"