From 1140a1af296200bcb78e455a9e33ba2ea5bf82e9 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Wed, 16 Apr 2025 10:49:29 -0700 Subject: [PATCH 01/17] WIP: xp_assert enhancements --- src/array_api_extra/_lib/_testing.py | 131 +++++++++++++++------------ tests/test_testing.py | 52 ++++++++++- 2 files changed, 122 insertions(+), 61 deletions(-) diff --git a/src/array_api_extra/_lib/_testing.py b/src/array_api_extra/_lib/_testing.py index 319297c8..36822892 100644 --- a/src/array_api_extra/_lib/_testing.py +++ b/src/array_api_extra/_lib/_testing.py @@ -9,6 +9,7 @@ from types import ModuleType from typing import cast +import numpy as np import pytest from ._utils._compat import ( @@ -16,6 +17,7 @@ is_array_api_strict_namespace, is_cupy_namespace, is_dask_namespace, + is_numpy_namespace, is_pydata_sparse_namespace, is_torch_namespace, ) @@ -25,7 +27,11 @@ def _check_ns_shape_dtype( - actual: Array, desired: Array + actual: Array, + desired: Array, + check_dtype: bool, + check_shape: bool, + check_scalar: bool, ) -> ModuleType: # numpydoc ignore=RT03 """ Assert that namespace, shape and dtype of the two arrays match. @@ -47,43 +53,64 @@ def _check_ns_shape_dtype( msg = f"namespaces do not match: {actual_xp} != f{desired_xp}" assert actual_xp == desired_xp, msg - actual_shape = actual.shape - desired_shape = desired.shape - if is_dask_namespace(desired_xp): - # Dask uses nan instead of None for unknown shapes - if any(math.isnan(i) for i in cast(tuple[float, ...], actual_shape)): - actual_shape = actual.compute().shape # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] - if any(math.isnan(i) for i in cast(tuple[float, ...], desired_shape)): - desired_shape = desired.compute().shape # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] - - msg = f"shapes do not match: {actual_shape} != f{desired_shape}" - assert actual_shape == desired_shape, msg - - msg = f"dtypes do not match: {actual.dtype} != {desired.dtype}" - assert actual.dtype == desired.dtype, msg + if check_shape: + actual_shape = actual.shape + desired_shape = desired.shape + if is_dask_namespace(desired_xp): + # Dask uses nan instead of None for unknown shapes + if any(math.isnan(i) for i in cast(tuple[float, ...], actual_shape)): + actual_shape = actual.compute().shape # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + if any(math.isnan(i) for i in cast(tuple[float, ...], desired_shape)): + desired_shape = desired.compute().shape # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + + msg = f"shapes do not match: {actual_shape} != f{desired_shape}" + assert actual_shape == desired_shape, msg + + if check_dtype: + msg = f"dtypes do not match: {actual.dtype} != {desired.dtype}" + assert actual.dtype == desired.dtype, msg + + if is_numpy_namespace(actual_xp) and check_scalar: + # only NumPy distinguishes between scalars and arrays; we do if check_scalar. + _msg = ( + "array-ness does not match:\n Actual: " + f"{type(actual)}\n Desired: {type(desired)}" + ) + assert (np.isscalar(actual) and np.isscalar(desired)) or ( + not np.isscalar(actual) and not np.isscalar(desired) + ), _msg return desired_xp def _prepare_for_test(array: Array, xp: ModuleType) -> Array: """ - Ensure that the array can be compared with xp.testing or np.testing. + Ensure that the array can be compared with np.testing. This involves transferring it from GPU to CPU memory, densifying it, etc. """ if is_torch_namespace(xp): - return array.cpu() # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + return np.asarray(array.cpu()) # type: ignore[attr-defined, return-value] # pyright: ignore[reportAttributeAccessIssue, reportUnknownArgumentType, reportReturnType] if is_pydata_sparse_namespace(xp): return array.todense() # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] if is_array_api_strict_namespace(xp): # Note: we deliberately did not add a `.to_device` method in _typing.pyi # even if it is required by the standard as many backends don't support it return array.to_device(xp.Device("CPU_DEVICE")) # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] - # Note: nothing to do for CuPy, because it uses a bespoke test function + if is_cupy_namespace(xp): + return xp.asnumpy(array) return array -def xp_assert_equal(actual: Array, desired: Array, err_msg: str = "") -> None: +def xp_assert_equal( + actual: Array, + desired: Array, + *, + err_msg: str = "", + check_dtype: bool = True, + check_shape: bool = True, + check_scalar: bool = False, +) -> None: """ Array-API compatible version of `np.testing.assert_array_equal`. @@ -95,34 +122,21 @@ def xp_assert_equal(actual: Array, desired: Array, err_msg: str = "") -> None: The expected array (typically hardcoded). err_msg : str, optional Error message to display on failure. + check_dtype, check_shape : bool, default: True + Whether to check agreement between actual and desired dtypes and shapes + check_scalar : bool, default: False + NumPy only: whether to check agreement between actual and desired types - + 0d array vs scalar. See Also -------- xp_assert_close : Similar function for inexact equality checks. numpy.testing.assert_array_equal : Similar function for NumPy arrays. """ - xp = _check_ns_shape_dtype(actual, desired) + xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) actual = _prepare_for_test(actual, xp) desired = _prepare_for_test(desired, xp) - - if is_cupy_namespace(xp): - xp.testing.assert_array_equal(actual, desired, err_msg=err_msg) - elif is_torch_namespace(xp): - # PyTorch recommends using `rtol=0, atol=0` like this - # to test for exact equality - xp.testing.assert_close( - actual, - desired, - rtol=0, - atol=0, - equal_nan=True, - check_dtype=False, - msg=err_msg or None, - ) - else: - import numpy as np # pylint: disable=import-outside-toplevel - - np.testing.assert_array_equal(actual, desired, err_msg=err_msg) + np.testing.assert_array_equal(actual, desired, err_msg=err_msg) def xp_assert_close( @@ -132,6 +146,9 @@ def xp_assert_close( rtol: float | None = None, atol: float = 0, err_msg: str = "", + check_dtype: bool = True, + check_shape: bool = True, + check_scalar: bool = False, ) -> None: """ Array-API compatible version of `np.testing.assert_allclose`. @@ -148,6 +165,11 @@ def xp_assert_close( Absolute tolerance. Default: 0. err_msg : str, optional Error message to display on failure. + check_dtype, check_shape : bool, default: True + Whether to check agreement between actual and desired dtypes and shapes + check_scalar : bool, default: False + NumPy only: whether to check agreement between actual and desired types - + 0d array vs scalar. See Also -------- @@ -159,7 +181,7 @@ def xp_assert_close( ----- The default `atol` and `rtol` differ from `xp.all(xpx.isclose(a, b))`. """ - xp = _check_ns_shape_dtype(actual, desired) + xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) floating = xp.isdtype(actual.dtype, ("real floating", "complex floating")) if rtol is None and floating: @@ -173,26 +195,15 @@ def xp_assert_close( actual = _prepare_for_test(actual, xp) desired = _prepare_for_test(desired, xp) - if is_cupy_namespace(xp): - xp.testing.assert_allclose( - actual, desired, rtol=rtol, atol=atol, err_msg=err_msg - ) - elif is_torch_namespace(xp): - xp.testing.assert_close( - actual, desired, rtol=rtol, atol=atol, equal_nan=True, msg=err_msg or None - ) - else: - import numpy as np # pylint: disable=import-outside-toplevel - - # JAX/Dask arrays work directly with `np.testing` - assert isinstance(rtol, float) - np.testing.assert_allclose( # type: ignore[call-overload] # pyright: ignore[reportCallIssue] - actual, # pyright: ignore[reportArgumentType] - desired, # pyright: ignore[reportArgumentType] - rtol=rtol, - atol=atol, - err_msg=err_msg, - ) + # JAX/Dask arrays work directly with `np.testing` + assert isinstance(rtol, float) + np.testing.assert_allclose( # type: ignore[call-overload] # pyright: ignore[reportCallIssue] + actual, # pyright: ignore[reportArgumentType] + desired, # pyright: ignore[reportArgumentType] + rtol=rtol, + atol=atol, + err_msg=err_msg, + ) def xfail(request: pytest.FixtureRequest, reason: str) -> None: diff --git a/tests/test_testing.py b/tests/test_testing.py index ff67121b..1f31d282 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,4 +1,5 @@ from collections.abc import Callable +from contextlib import nullcontext from types import ModuleType from typing import cast @@ -24,7 +25,9 @@ xp_assert_equal, pytest.param( xp_assert_close, - marks=pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype"), + marks=pytest.mark.xfail_xp_backend( + Backend.SPARSE, reason="no isdtype", strict=False + ), ), ], ) @@ -60,6 +63,53 @@ def test_assert_close_equal_namespace(xp: ModuleType, func: Callable[..., None]) func(xp.asarray([0]), [0]) +@param_assert_equal_close +@pytest.mark.parametrize("check_shape", [False, True]) +def test_assert_close_equal_shape( # type: ignore[explicit-any] + xp: ModuleType, + func: Callable[..., None], + check_shape: bool, +): + context = ( + pytest.raises(AssertionError, match="shapes do not match") + if check_shape + else nullcontext() + ) + with context: + func(xp.asarray([0, 0]), xp.asarray(0), check_shape=check_shape) + + +@param_assert_equal_close +@pytest.mark.parametrize("check_dtype", [False, True]) +def test_assert_close_equal_dtype( # type: ignore[explicit-any] + xp: ModuleType, + func: Callable[..., None], + check_dtype: bool, +): + context = ( + pytest.raises(AssertionError, match="dtypes do not match") + if check_dtype + else nullcontext() + ) + with context: + func(xp.asarray(0.0), xp.asarray(0), check_dtype=check_dtype) + + +@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) +@pytest.mark.parametrize("check_scalar", [False, True]) +def test_assert_close_equal_scalar( # type: ignore[explicit-any] + func: Callable[..., None], + check_scalar: bool, +): + context = ( + pytest.raises(AssertionError, match="array-ness does not match") + if check_scalar + else nullcontext() + ) + with context: + func(np.asarray(0), np.asarray(0)[()], check_scalar=check_scalar) + + @pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype") def test_assert_close_tolerance(xp: ModuleType): xp_assert_close(xp.asarray([100.0]), xp.asarray([102.0]), rtol=0.03) From 9c9326adb81efa1b48b2ab1cc7f91c4916e2d7f1 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Mon, 21 Apr 2025 15:55:57 +0100 Subject: [PATCH 02/17] TST: `xfail_xp_backend(strict=False)` (#269) --- pixi.lock | 2 +- pyproject.toml | 4 ++-- src/array_api_extra/_lib/_testing.py | 14 +++++++++++-- tests/conftest.py | 30 ++++++++++++++++++---------- tests/test_at.py | 12 +++++++---- tests/test_funcs.py | 2 +- tests/test_helpers.py | 2 +- 7 files changed, 44 insertions(+), 22 deletions(-) diff --git a/pixi.lock b/pixi.lock index 56ee7c56..2153c4e2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: 74777bddfe6ab8d3ced9e5d1c645cb95c637707a45de9e96c88fc3b41723e3af + sha256: 68490b5f2feb7687422f882f54bb2a93c687425b984a69ecd58c9d6d73653139 requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index 67651904..9d897cc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,8 +213,8 @@ filterwarnings = ["error"] log_cli_level = "INFO" testpaths = ["tests"] markers = [ - "skip_xp_backend(library, *, reason=None): Skip test for a specific backend", - "xfail_xp_backend(library, *, reason=None): Xfail test for a specific backend", + "skip_xp_backend(library, /, *, reason=None): Skip test for a specific backend", + "xfail_xp_backend(library, /, *, reason=None, strict=None): Xfail test for a specific backend", ] diff --git a/src/array_api_extra/_lib/_testing.py b/src/array_api_extra/_lib/_testing.py index 319297c8..301a851f 100644 --- a/src/array_api_extra/_lib/_testing.py +++ b/src/array_api_extra/_lib/_testing.py @@ -195,7 +195,9 @@ def xp_assert_close( ) -def xfail(request: pytest.FixtureRequest, reason: str) -> None: +def xfail( + request: pytest.FixtureRequest, *, reason: str, strict: bool | None = None +) -> None: """ XFAIL the currently running test. @@ -209,5 +211,13 @@ def xfail(request: pytest.FixtureRequest, reason: str) -> None: ``request`` argument of the test function. reason : str Reason for the expected failure. + strict: bool, optional + If True, the test will be marked as failed if it passes. + If False, the test will be marked as passed if it fails. + Default: ``xfail_strict`` value in ``pyproject.toml``, or False if absent. """ - request.node.add_marker(pytest.mark.xfail(reason=reason)) + if strict is not None: + marker = pytest.mark.xfail(reason=reason, strict=strict) + else: + marker = pytest.mark.xfail(reason=reason) + request.node.add_marker(marker) diff --git a/tests/conftest.py b/tests/conftest.py index 410a87ff..5676cc0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,6 @@ """Pytest fixtures.""" from collections.abc import Callable, Generator -from contextlib import suppress from functools import partial, wraps from types import ModuleType from typing import ParamSpec, TypeVar, cast @@ -34,20 +33,29 @@ def library(request: pytest.FixtureRequest) -> Backend: # numpydoc ignore=PR01, """ elem = cast(Backend, request.param) - for marker_name, skip_or_xfail in ( - ("skip_xp_backend", pytest.skip), - ("xfail_xp_backend", partial(xfail, request)), + for marker_name, skip_or_xfail, allow_kwargs in ( + ("skip_xp_backend", pytest.skip, {"reason"}), + ("xfail_xp_backend", partial(xfail, request), {"reason", "strict"}), ): for marker in request.node.iter_markers(marker_name): - library = marker.kwargs.get("library") or marker.args[0] # type: ignore[no-untyped-usage] - if not isinstance(library, Backend): - msg = f"argument of {marker_name} must be a Backend enum" + if len(marker.args) != 1: # pyright: ignore[reportUnknownArgumentType] + msg = f"Expected exactly one positional argument; got {marker.args}" raise TypeError(msg) + if not isinstance(marker.args[0], Backend): + msg = f"Argument of {marker_name} must be a Backend enum" + raise TypeError(msg) + if invalid_kwargs := set(marker.kwargs) - allow_kwargs: # pyright: ignore[reportUnknownArgumentType] + msg = f"Unexpected kwarg(s): {invalid_kwargs}" + raise TypeError(msg) + + library: Backend = marker.args[0] + reason: str | None = marker.kwargs.get("reason", None) + strict: bool | None = marker.kwargs.get("strict", None) + if library == elem: - reason = str(library) - with suppress(KeyError): - reason += ":" + cast(str, marker.kwargs["reason"]) - skip_or_xfail(reason=reason) + reason = f"{library}: {reason}" if reason else str(library) # pyright: ignore[reportUnknownArgumentType] + kwargs = {"strict": strict} if strict is not None else {} + skip_or_xfail(reason=reason, **kwargs) # pyright: ignore[reportUnknownArgumentType] return elem diff --git a/tests/test_at.py b/tests/test_at.py index 4ccf584e..fa9bcdc8 100644 --- a/tests/test_at.py +++ b/tests/test_at.py @@ -115,11 +115,15 @@ def assert_copy( pytest.param( *(True, 1, 1), marks=( - pytest.mark.skip_xp_backend( # test passes when copy=False - Backend.JAX, reason="bool mask update with shaped rhs" + pytest.mark.xfail_xp_backend( + Backend.JAX, + reason="bool mask update with shaped rhs", + strict=False, # test passes when copy=False ), - pytest.mark.skip_xp_backend( # test passes when copy=False - Backend.JAX_GPU, reason="bool mask update with shaped rhs" + pytest.mark.xfail_xp_backend( + Backend.JAX_GPU, + reason="bool mask update with shaped rhs", + strict=False, # test passes when copy=False ), pytest.mark.xfail_xp_backend( Backend.DASK, reason="bool mask update with shaped rhs" diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 4e40f09b..652e12ef 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -196,7 +196,7 @@ def test_device(self, xp: ModuleType, device: Device): y = apply_where(x % 2 == 0, x, self.f1, fill_value=x) assert get_device(y) == device - @pytest.mark.skip_xp_backend(Backend.SPARSE, reason="no isdtype") + @pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype") @pytest.mark.filterwarnings("ignore::RuntimeWarning") # overflows, etc. @hypothesis.settings( # The xp and library fixtures are not regenerated between hypothesis iterations diff --git a/tests/test_helpers.py b/tests/test_helpers.py index ebd4811f..a104e93c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -27,7 +27,7 @@ lazy_xp_function(in1d, jax_jit=False, static_argnames=("assume_unique", "invert", "xp")) -@pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no unique_inverse") +@pytest.mark.skip_xp_backend(Backend.SPARSE, reason="no unique_inverse") @pytest.mark.skip_xp_backend(Backend.ARRAY_API_STRICTEST, reason="no unique_inverse") class TestIn1D: # cover both code paths From d7f754931276c50969ebbf70bf404ac96216c5d4 Mon Sep 17 00:00:00 2001 From: Matt Haberland Date: Mon, 21 Apr 2025 10:47:54 -0700 Subject: [PATCH 03/17] ENH: add xp_assert_less --- src/array_api_extra/_lib/_testing.py | 40 ++++++++++++++++++++++--- tests/test_testing.py | 44 ++++++++++++++++++++-------- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/array_api_extra/_lib/_testing.py b/src/array_api_extra/_lib/_testing.py index 41eeddc7..c7f700ed 100644 --- a/src/array_api_extra/_lib/_testing.py +++ b/src/array_api_extra/_lib/_testing.py @@ -76,9 +76,7 @@ def _check_ns_shape_dtype( "array-ness does not match:\n Actual: " f"{type(actual)}\n Desired: {type(desired)}" ) - assert (np.isscalar(actual) and np.isscalar(desired)) or ( - not np.isscalar(actual) and not np.isscalar(desired) - ), _msg + assert np.isscalar(actual) == np.isscalar(desired), _msg return desired_xp @@ -139,6 +137,41 @@ def xp_assert_equal( np.testing.assert_array_equal(actual, desired, err_msg=err_msg) +def xp_assert_less( + x: Array, + y: Array, + *, + err_msg: str = "", + check_dtype: bool = True, + check_shape: bool = True, + check_scalar: bool = False, +) -> None: + """ + Array-API compatible version of `np.testing.assert_array_less`. + + Parameters + ---------- + x, y : Array + The arrays to compare according to ``x < y`` (elementwise). + err_msg : str, optional + Error message to display on failure. + check_dtype, check_shape : bool, default: True + Whether to check agreement between actual and desired dtypes and shapes + check_scalar : bool, default: False + NumPy only: whether to check agreement between actual and desired types - + 0d array vs scalar. + + See Also + -------- + xp_assert_close : Similar function for inexact equality checks. + numpy.testing.assert_array_equal : Similar function for NumPy arrays. + """ + xp = _check_ns_shape_dtype(x, y, check_dtype, check_shape, check_scalar) + x = _prepare_for_test(x, xp) + y = _prepare_for_test(y, xp) + np.testing.assert_array_less(x, y, err_msg=err_msg) # type: ignore[call-overload] + + def xp_assert_close( actual: Array, desired: Array, @@ -196,7 +229,6 @@ def xp_assert_close( desired = _prepare_for_test(desired, xp) # JAX/Dask arrays work directly with `np.testing` - assert isinstance(rtol, float) np.testing.assert_allclose( # type: ignore[call-overload] # pyright: ignore[reportCallIssue] actual, # pyright: ignore[reportArgumentType] desired, # pyright: ignore[reportArgumentType] diff --git a/tests/test_testing.py b/tests/test_testing.py index 1f31d282..a5dd12d1 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -7,7 +7,11 @@ import pytest from array_api_extra._lib._backends import Backend -from array_api_extra._lib._testing import xp_assert_close, xp_assert_equal +from array_api_extra._lib._testing import ( + xp_assert_close, + xp_assert_equal, + xp_assert_less, +) from array_api_extra._lib._utils._compat import ( array_namespace, is_dask_namespace, @@ -23,6 +27,7 @@ "func", [ xp_assert_equal, + xp_assert_less, pytest.param( xp_assert_close, marks=pytest.mark.xfail_xp_backend( @@ -33,7 +38,8 @@ ) -@param_assert_equal_close +@pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype", strict=False) +@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) def test_assert_close_equal_basic(xp: ModuleType, func: Callable[..., None]): # type: ignore[explicit-any] func(xp.asarray(0), xp.asarray(0)) func(xp.asarray([1, 2]), xp.asarray([1, 2])) @@ -53,8 +59,8 @@ def test_assert_close_equal_basic(xp: ModuleType, func: Callable[..., None]): # @pytest.mark.skip_xp_backend(Backend.NUMPY, reason="test other ns vs. numpy") @pytest.mark.skip_xp_backend(Backend.NUMPY_READONLY, reason="test other ns vs. numpy") -@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) -def test_assert_close_equal_namespace(xp: ModuleType, func: Callable[..., None]): # type: ignore[explicit-any] +@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close, xp_assert_less]) +def test_assert_close_equal_less_namespace(xp: ModuleType, func: Callable[..., None]): # type: ignore[explicit-any] with pytest.raises(AssertionError, match="namespaces do not match"): func(xp.asarray(0), np.asarray(0)) with pytest.raises(TypeError, match="Unrecognized array input"): @@ -65,7 +71,7 @@ def test_assert_close_equal_namespace(xp: ModuleType, func: Callable[..., None]) @param_assert_equal_close @pytest.mark.parametrize("check_shape", [False, True]) -def test_assert_close_equal_shape( # type: ignore[explicit-any] +def test_assert_close_equal_less_shape( # type: ignore[explicit-any] xp: ModuleType, func: Callable[..., None], check_shape: bool, @@ -76,12 +82,12 @@ def test_assert_close_equal_shape( # type: ignore[explicit-any] else nullcontext() ) with context: - func(xp.asarray([0, 0]), xp.asarray(0), check_shape=check_shape) + func(xp.asarray([xp.nan, xp.nan]), xp.asarray(xp.nan), check_shape=check_shape) @param_assert_equal_close @pytest.mark.parametrize("check_dtype", [False, True]) -def test_assert_close_equal_dtype( # type: ignore[explicit-any] +def test_assert_close_equal_less_dtype( # type: ignore[explicit-any] xp: ModuleType, func: Callable[..., None], check_dtype: bool, @@ -92,12 +98,17 @@ def test_assert_close_equal_dtype( # type: ignore[explicit-any] else nullcontext() ) with context: - func(xp.asarray(0.0), xp.asarray(0), check_dtype=check_dtype) + func( + xp.asarray(xp.nan, dtype=xp.float32), + xp.asarray(xp.nan, dtype=xp.float64), + check_dtype=check_dtype, + ) -@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) +@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close, xp_assert_less]) @pytest.mark.parametrize("check_scalar", [False, True]) -def test_assert_close_equal_scalar( # type: ignore[explicit-any] +def test_assert_close_equal_less_scalar( # type: ignore[explicit-any] + xp: ModuleType, func: Callable[..., None], check_scalar: bool, ): @@ -107,7 +118,7 @@ def test_assert_close_equal_scalar( # type: ignore[explicit-any] else nullcontext() ) with context: - func(np.asarray(0), np.asarray(0)[()], check_scalar=check_scalar) + func(np.asarray(xp.nan), np.asarray(xp.nan)[()], check_scalar=check_scalar) @pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype") @@ -121,9 +132,18 @@ def test_assert_close_tolerance(xp: ModuleType): xp_assert_close(xp.asarray([100.0]), xp.asarray([102.0]), atol=1) -@param_assert_equal_close +def test_assert_less_basic(xp: ModuleType): + xp_assert_less(xp.asarray(-1), xp.asarray(0)) + xp_assert_less(xp.asarray([1, 2]), xp.asarray([2, 3])) + with pytest.raises(AssertionError): + xp_assert_less(xp.asarray([1, 1]), xp.asarray([2, 1])) + with pytest.raises(AssertionError, match="hello"): + xp_assert_less(xp.asarray([1, 1]), xp.asarray([2, 1]), err_msg="hello") + + @pytest.mark.skip_xp_backend(Backend.SPARSE, reason="index by sparse array") @pytest.mark.skip_xp_backend(Backend.ARRAY_API_STRICTEST, reason="boolean indexing") +@pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) def test_assert_close_equal_none_shape(xp: ModuleType, func: Callable[..., None]): # type: ignore[explicit-any] """On Dask and other lazy backends, test that a shape with NaN's or None's can be compared to a real shape. From 6998deb01f5a78d2f0c1285c10c110eb698cf8e2 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Tue, 22 Apr 2025 16:50:28 +0100 Subject: [PATCH 04/17] TST: flaky `test_single_axis` (#272) --- tests/test_funcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 652e12ef..0cee0b4d 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -521,7 +521,7 @@ def test_xp(self, xp: ModuleType): class TestExpandDims: def test_single_axis(self, xp: ModuleType): """Trivial case where xpx.expand_dims doesn't add anything to xp.expand_dims""" - a = xp.empty((2, 3, 4, 5)) + a = xp.asarray(np.reshape(np.arange(2 * 3 * 4 * 5), (2, 3, 4, 5))) for axis in range(-5, 4): b = expand_dims(a, axis=axis) xp_assert_equal(b, xp.expand_dims(a, axis=axis)) From 2ba372c6b08ad32fcabef58403d33114c500e9a5 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Tue, 22 Apr 2025 20:29:41 +0100 Subject: [PATCH 05/17] CD: fix docs deployment (#273) closes gh-266 --- .github/workflows/docs-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index 3f7e6ed0..78a5ada1 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -1,7 +1,7 @@ name: Docs Deploy permissions: - contents: read + contents: write # needed for the deploy step on: workflow_run: From 675582f74420ed260d8d079b8d5244d26aa660c0 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Wed, 23 Apr 2025 15:06:07 +0100 Subject: [PATCH 06/17] deps: add dep groups for dask and jax (#275) --- renovate.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/renovate.json b/renovate.json index 10ea8ab9..473f963c 100644 --- a/renovate.json +++ b/renovate.json @@ -50,6 +50,14 @@ "matchManagers": ["github-actions"], "matchPackageNames": ["python"], "enabled": false + }, + { + "matchPackageNames": ["dask", "dask-core"], + "groupName": "dask" + }, + { + "matchPackageNames": ["jax", "jaxlib"], + "groupName": "jax" } ] } From 143ff393c57516c914e786ddb6309a5a32b477ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:19:07 +0000 Subject: [PATCH 07/17] deps: Update dependency hypothesis to >=6.130.12 (#274) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pixi.lock b/pixi.lock index 2153c4e2..487c161a 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: 68490b5f2feb7687422f882f54bb2a93c687425b984a69ecd58c9d6d73653139 + sha256: ba6f94790cc0ad792e4857f421b2bed4c62892f2bc923db0108ff6b34c92ddef requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index 9d897cc0..058b2467 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ numpydoc = ">=1.8.0,<2" array-api-strict = ">=2.3.1" numpy = ">=2.1.3" pytest = ">=8.3.5" -hypothesis = ">=6.130.11" +hypothesis = ">=6.130.12" dask-core = ">=2025.3.0" # No distributed, tornado, etc. # NOTE: don't add jax, pytorch, sparse, cupy here # as they slow down mypy and are not portable across target OSs @@ -80,7 +80,7 @@ lint = { depends-on = ["pre-commit", "pylint", "mypy", "pyright"] , description [tool.pixi.feature.tests.dependencies] pytest = ">=8.3.5" pytest-cov = ">=6.1.1" -hypothesis = ">=6.130.11" +hypothesis = ">=6.130.12" array-api-strict = ">=2.3.1" numpy = ">=1.22.0" From 53db9db57c60abd52614afe8b51208e20d09c2f3 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Wed, 23 Apr 2025 15:22:02 +0100 Subject: [PATCH 08/17] DEV: disable codecov project status (#276) this avoids failing statuses on PRs due to having to wait for test-backends in unrelated areas of the codebase --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov.yml b/codecov.yml index d05bc8e2..dc9b47cd 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,3 +4,6 @@ github_checks: ignore: - "src/array_api_extra/_lib/_compat" - "src/array_api_extra/_lib/_typing" +coverage: + status: + project: off From 54aaf84ebc90465249ad14d6784ae373ca68ef8f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 04:21:04 +0000 Subject: [PATCH 09/17] deps: Update dependency basedpyright to >=1.28.5 (#277) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index 487c161a..aeb86565 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: ba6f94790cc0ad792e4857f421b2bed4c62892f2bc923db0108ff6b34c92ddef + sha256: e0b7b737906dee4bde7c104b7c777f78a22a1b296f32ba74da5b72cce828eb63 requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index 058b2467..82290941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ typing-extensions = ">=4.13.1" pre-commit = ">=4.2.0" pylint = ">=3.3.6" basedmypy = ">=2.10.0" -basedpyright = ">=1.28.3" +basedpyright = ">=1.28.5" numpydoc = ">=1.8.0,<2" # import dependencies for mypy: array-api-strict = ">=2.3.1" From ebcdaca65bbf5fa78dd76ef16afeb3e51937badb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 10:58:24 +0000 Subject: [PATCH 10/17] deps: Update dependency numba to >=0.61.2 (#278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index aeb86565..58f0ca0f 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: e0b7b737906dee4bde7c104b7c777f78a22a1b296f32ba74da5b72cce828eb63 + sha256: 620e3454c349ccc780eefd0a1aa3ab21e1e574465484b43c089402f12f71e3e6 requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index 82290941..df9b6b0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ numpy = "=1.22.0" [tool.pixi.feature.backends.dependencies] pytorch = ">=2.6.0" dask = ">=2025.3.0" -numba = ">=0.61.0" # sparse dependency +numba = ">=0.61.2" # sparse dependency llvmlite = ">=0.44.0" # sparse dependency [tool.pixi.feature.backends.pypi-dependencies] From 9a425121bbf6afb5bd155e137ac65b8aa5494926 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:49:58 +0000 Subject: [PATCH 11/17] deps: Update dependency typing-extensions to >=4.13.2 (#279) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pixi.lock b/pixi.lock index 58f0ca0f..fbe6f8b8 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: 620e3454c349ccc780eefd0a1aa3ab21e1e574465484b43c089402f12f71e3e6 + sha256: 3c9987a48df7f5e5f16e25b7b14512512f23c4438e62c85f5c830973620138ca requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index df9b6b0e..e560ff2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ array-api-compat = ">=1.11.2,<2" array-api-extra = { path = ".", editable = true } [tool.pixi.feature.lint.dependencies] -typing-extensions = ">=4.13.1" +typing-extensions = ">=4.13.2" pre-commit = ">=4.2.0" pylint = ">=3.3.6" basedmypy = ">=2.10.0" @@ -107,7 +107,7 @@ sphinx-autodoc-typehints = ">=1.25.3" # Needed to import parsed modules with autodoc dask-core = ">=2025.3.0" pytest = ">=8.3.5" -typing-extensions = ">=4.13.1" +typing-extensions = ">=4.13.2" numpy = ">=2.1.3" [tool.pixi.feature.docs.tasks] From 375429280779ac8b0d6bef508a695428cba626f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 21:51:55 +0100 Subject: [PATCH 12/17] deps: Update dependency hypothesis to >=6.131.0 (#280) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pixi.lock b/pixi.lock index fbe6f8b8..098b77a2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: 3c9987a48df7f5e5f16e25b7b14512512f23c4438e62c85f5c830973620138ca + sha256: 9d5f699a813b67c48b9b575fcc37ccd15092cfbf685005c28ab952d29096d05b requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' diff --git a/pyproject.toml b/pyproject.toml index e560ff2d..b87bfc57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ numpydoc = ">=1.8.0,<2" array-api-strict = ">=2.3.1" numpy = ">=2.1.3" pytest = ">=8.3.5" -hypothesis = ">=6.130.12" +hypothesis = ">=6.131.0" dask-core = ">=2025.3.0" # No distributed, tornado, etc. # NOTE: don't add jax, pytorch, sparse, cupy here # as they slow down mypy and are not portable across target OSs @@ -80,7 +80,7 @@ lint = { depends-on = ["pre-commit", "pylint", "mypy", "pyright"] , description [tool.pixi.feature.tests.dependencies] pytest = ">=8.3.5" pytest-cov = ">=6.1.1" -hypothesis = ">=6.130.12" +hypothesis = ">=6.131.0" array-api-strict = ">=2.3.1" numpy = ">=1.22.0" From 22234b4593be902efd494909705fbe321613c1f5 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Thu, 24 Apr 2025 21:52:22 +0100 Subject: [PATCH 13/17] deps: schedule hypothesis monthly (#281) releases are very frequent, causing noise --- renovate.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/renovate.json b/renovate.json index 473f963c..05342e62 100644 --- a/renovate.json +++ b/renovate.json @@ -52,12 +52,20 @@ "enabled": false }, { + "description": "Group Dask packages.", "matchPackageNames": ["dask", "dask-core"], "groupName": "dask" }, { + "description": "Group JAX packages.", "matchPackageNames": ["jax", "jaxlib"], "groupName": "jax" + }, + { + "description": "Schedule hypothesis monthly as releases are frequent.", + "matchManagers": ["pixi"], + "matchPackageNames": ["hypothesis"], + "schedule": ["* * 10 * *"] } ] } From 93763994120c4c57b53f4d65d59f5dc21cfa9f16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 22:02:38 +0100 Subject: [PATCH 14/17] deps: Update dependency hypothesis to >=6.131.8 (#282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pixi.lock | 85 +++++++++++++++++++++++++------------------------- pyproject.toml | 4 +-- 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pixi.lock b/pixi.lock index 098b77a2..61b1f717 100644 --- a/pixi.lock +++ b/pixi.lock @@ -159,7 +159,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -415,7 +415,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -662,7 +662,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -899,7 +899,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 @@ -1159,7 +1159,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -1439,7 +1439,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -1686,7 +1686,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -1935,7 +1935,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 @@ -2512,7 +2512,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -2625,7 +2625,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -2733,7 +2733,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda @@ -2841,7 +2841,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/identify-2.6.9-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/imagesize-1.4.1-pyhd8ed1ab_0.tar.bz2 @@ -2939,7 +2939,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/coverage-7.8.0-py313h8060acc_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda @@ -2986,7 +2986,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/coverage-7.8.0-py313h717bdf5_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/libblas-3.9.0-31_h7f60823_openblas.conda - conda: https://prefix.dev/conda-forge/osx-64/libcblas-3.9.0-31_hff6cab4_openblas.conda @@ -3029,7 +3029,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/coverage-7.8.0-py313ha9b7d5b_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.9.0-31_h10e41b3_openblas.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.9.0-31_hb3479ef_openblas.conda @@ -3072,7 +3072,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/coverage-7.8.0-py313hb4c8b1a_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda - conda: https://prefix.dev/conda-forge/win-64/libblas-3.9.0-31_h641d27c_mkl.conda @@ -3164,7 +3164,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -3356,7 +3356,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -3539,7 +3539,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -3712,7 +3712,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda @@ -3910,7 +3910,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -4126,7 +4126,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -4309,7 +4309,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda @@ -4494,7 +4494,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda @@ -4641,7 +4641,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/coverage-7.8.0-py310h89163eb_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda @@ -4690,7 +4690,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/coverage-7.8.0-py310h8e2f543_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/libblas-3.9.0-31_h7f60823_openblas.conda - conda: https://prefix.dev/conda-forge/osx-64/libcblas-3.9.0-31_hff6cab4_openblas.conda @@ -4732,7 +4732,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/coverage-7.8.0-py310hc74094e_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.9.0-31_h10e41b3_openblas.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.9.0-31_hb3479ef_openblas.conda @@ -4774,7 +4774,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/coverage-7.8.0-py310h38315fa_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda - conda: https://prefix.dev/conda-forge/win-64/libblas-3.9.0-31_h641d27c_mkl.conda @@ -4828,7 +4828,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/coverage-7.8.0-py310h89163eb_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda @@ -4876,7 +4876,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/coverage-7.8.0-py310h8e2f543_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/libblas-3.9.0-31_h7f60823_openblas.conda - conda: https://prefix.dev/conda-forge/osx-64/libcblas-3.9.0-31_hff6cab4_openblas.conda @@ -4918,7 +4918,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/coverage-7.8.0-py310hc74094e_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.9.0-31_h10e41b3_openblas.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.9.0-31_hb3479ef_openblas.conda @@ -4960,7 +4960,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/coverage-7.8.0-py310h38315fa_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda - conda: https://prefix.dev/conda-forge/win-64/libblas-3.9.0-31_h641d27c_mkl.conda @@ -5013,7 +5013,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/coverage-7.8.0-py313h8060acc_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda - conda: https://prefix.dev/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda @@ -5060,7 +5060,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/coverage-7.8.0-py313h717bdf5_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/libblas-3.9.0-31_h7f60823_openblas.conda - conda: https://prefix.dev/conda-forge/osx-64/libcblas-3.9.0-31_hff6cab4_openblas.conda @@ -5103,7 +5103,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/coverage-7.8.0-py313ha9b7d5b_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libblas-3.9.0-31_h10e41b3_openblas.conda - conda: https://prefix.dev/conda-forge/osx-arm64/libcblas-3.9.0-31_hb3479ef_openblas.conda @@ -5146,7 +5146,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/coverage-7.8.0-py313hb4c8b1a_0.conda - conda: https://prefix.dev/conda-forge/noarch/exceptiongroup-1.2.2-pyhd8ed1ab_1.conda - - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda + - conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda - conda: https://prefix.dev/conda-forge/win-64/libblas-3.9.0-31_h641d27c_mkl.conda @@ -5256,7 +5256,7 @@ packages: - pypi: . name: array-api-extra version: 0.7.2.dev0 - sha256: 9d5f699a813b67c48b9b575fcc37ccd15092cfbf685005c28ab952d29096d05b + sha256: eb518a1094740e5a41c947fb7b93845d39c8c52fd03755313440f3771ecad7f6 requires_dist: - array-api-compat>=1.11.2,<2 requires_python: '>=3.10' @@ -8001,9 +8001,9 @@ packages: - pkg:pypi/hyperframe?source=hash-mapping size: 17397 timestamp: 1737618427549 -- conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.0-pyha770c72_0.conda - sha256: 10ba30fee960f8e02b49f030d1272e41694752ed6bd6260be611611c5f03d376 - md5: fdb4b15c1f542fb91da87f8b6f6535de +- conda: https://prefix.dev/conda-forge/noarch/hypothesis-6.131.8-pyha770c72_0.conda + sha256: 420637353239732b2649bf3ed6039bf7e12f09f595752a67d5d27be72b88e86b + md5: 09f4414e824e694fb3b89b25421b27df depends: - attrs >=22.2.0 - click >=7.0 @@ -8012,11 +8012,10 @@ packages: - setuptools - sortedcontainers >=2.1.0,<3.0.0 license: MPL-2.0 - license_family: MOZILLA purls: - pkg:pypi/hypothesis?source=hash-mapping - size: 352719 - timestamp: 1744300918665 + size: 356193 + timestamp: 1745475780825 - conda: https://prefix.dev/conda-forge/linux-64/icu-75.1-he02047a_0.conda sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e md5: 8b189310083baabfb622af68fd9d3ae3 diff --git a/pyproject.toml b/pyproject.toml index b87bfc57..cba9c4cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ numpydoc = ">=1.8.0,<2" array-api-strict = ">=2.3.1" numpy = ">=2.1.3" pytest = ">=8.3.5" -hypothesis = ">=6.131.0" +hypothesis = ">=6.131.8" dask-core = ">=2025.3.0" # No distributed, tornado, etc. # NOTE: don't add jax, pytorch, sparse, cupy here # as they slow down mypy and are not portable across target OSs @@ -80,7 +80,7 @@ lint = { depends-on = ["pre-commit", "pylint", "mypy", "pyright"] , description [tool.pixi.feature.tests.dependencies] pytest = ">=8.3.5" pytest-cov = ">=6.1.1" -hypothesis = ">=6.131.0" +hypothesis = ">=6.131.8" array-api-strict = ">=2.3.1" numpy = ">=1.22.0" From 4425d149867770a28b316fd98133c2b4584bfc48 Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 25 Apr 2025 13:18:52 +0100 Subject: [PATCH 15/17] ENH: `allow_dask_compute=True` instead of 999 (#283) --- src/array_api_extra/testing.py | 18 +++++++++++++----- tests/test_testing.py | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/array_api_extra/testing.py b/src/array_api_extra/testing.py index 4f8288cf..37e8e69e 100644 --- a/src/array_api_extra/testing.py +++ b/src/array_api_extra/testing.py @@ -39,7 +39,7 @@ def override(func: object) -> object: def lazy_xp_function( # type: ignore[explicit-any] func: Callable[..., Any], *, - allow_dask_compute: int = 0, + allow_dask_compute: bool | int = False, jax_jit: bool = True, static_argnums: int | Sequence[int] | None = None, static_argnames: str | Iterable[str] | None = None, @@ -59,9 +59,10 @@ def lazy_xp_function( # type: ignore[explicit-any] ---------- func : callable Function to be tested. - allow_dask_compute : int, optional - Number of times `func` is allowed to internally materialize the Dask graph. This - is typically triggered by ``bool()``, ``float()``, or ``np.asarray()``. + allow_dask_compute : bool | int, optional + Whether `func` is allowed to internally materialize the Dask graph, or maximum + number of times it is allowed to do so. This is typically triggered by + ``bool()``, ``float()``, or ``np.asarray()``. Set to 1 if you are aware that `func` converts the input parameters to NumPy and want to let it do so at least for the time being, knowing that it is going to be @@ -75,7 +76,10 @@ def lazy_xp_function( # type: ignore[explicit-any] a test function that invokes `func` multiple times should still work with this parameter set to 1. - Default: 0, meaning that `func` must be fully lazy and never materialize the + Set to True to allow `func` to materialize the graph an unlimited number + of times. + + Default: False, meaning that `func` must be fully lazy and never materialize the graph. jax_jit : bool, optional Set to True to replace `func` with ``jax.jit(func)`` after calling the @@ -235,6 +239,10 @@ def iter_tagged() -> ( # type: ignore[explicit-any] if is_dask_namespace(xp): for mod, name, func, tags in iter_tagged(): n = tags["allow_dask_compute"] + if n is True: + n = 1_000_000 + elif n is False: + n = 0 wrapped = _dask_wrap(func, n) monkeypatch.setattr(mod, name, wrapped) diff --git a/tests/test_testing.py b/tests/test_testing.py index ff67121b..fb9ba581 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -130,13 +130,18 @@ def non_materializable4(x: Array) -> Array: return non_materializable(x) +def non_materializable5(x: Array) -> Array: + return non_materializable(x) + + lazy_xp_function(good_lazy) # Works on JAX and Dask lazy_xp_function(non_materializable2, jax_jit=False, allow_dask_compute=2) +lazy_xp_function(non_materializable3, jax_jit=False, allow_dask_compute=True) # Works on JAX, but not Dask -lazy_xp_function(non_materializable3, jax_jit=False, allow_dask_compute=1) +lazy_xp_function(non_materializable4, jax_jit=False, allow_dask_compute=1) # Works neither on Dask nor JAX -lazy_xp_function(non_materializable4) +lazy_xp_function(non_materializable5) def test_lazy_xp_function(xp: ModuleType): @@ -147,29 +152,30 @@ def test_lazy_xp_function(xp: ModuleType): xp_assert_equal(non_materializable(x), xp.asarray([1.0, 2.0])) # Wrapping explicitly disabled xp_assert_equal(non_materializable2(x), xp.asarray([1.0, 2.0])) + xp_assert_equal(non_materializable3(x), xp.asarray([1.0, 2.0])) if is_jax_namespace(xp): - xp_assert_equal(non_materializable3(x), xp.asarray([1.0, 2.0])) + xp_assert_equal(non_materializable4(x), xp.asarray([1.0, 2.0])) with pytest.raises( TypeError, match="Attempted boolean conversion of traced array" ): - _ = non_materializable4(x) # Wrapped + _ = non_materializable5(x) # Wrapped elif is_dask_namespace(xp): with pytest.raises( AssertionError, match=r"dask\.compute.* 2 times, but only up to 1 calls are allowed", ): - _ = non_materializable3(x) + _ = non_materializable4(x) with pytest.raises( AssertionError, match=r"dask\.compute.* 1 times, but no calls are allowed", ): - _ = non_materializable4(x) + _ = non_materializable5(x) else: - xp_assert_equal(non_materializable3(x), xp.asarray([1.0, 2.0])) xp_assert_equal(non_materializable4(x), xp.asarray([1.0, 2.0])) + xp_assert_equal(non_materializable5(x), xp.asarray([1.0, 2.0])) def static_params(x: Array, n: int, flag: bool = False) -> Array: From bb6129b1bfe344b9807a2f28451fe9211efe0b1b Mon Sep 17 00:00:00 2001 From: Guido Imperiale Date: Fri, 25 Apr 2025 15:57:08 +0100 Subject: [PATCH 16/17] MAINT: array_api_compat tweaks (#285) --- src/array_api_extra/_lib/_utils/_compat.py | 3 ++ src/array_api_extra/_lib/_utils/_compat.pyi | 43 ++++++++++++--------- vendor_tests/test_vendor.py | 3 +- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/array_api_extra/_lib/_utils/_compat.py b/src/array_api_extra/_lib/_utils/_compat.py index b9997450..c6eec4cd 100644 --- a/src/array_api_extra/_lib/_utils/_compat.py +++ b/src/array_api_extra/_lib/_utils/_compat.py @@ -23,6 +23,7 @@ is_torch_namespace, is_writeable_array, size, + to_device, ) except ImportError: from array_api_compat import ( @@ -45,6 +46,7 @@ is_torch_namespace, is_writeable_array, size, + to_device, ) __all__ = [ @@ -67,4 +69,5 @@ "is_torch_namespace", "is_writeable_array", "size", + "to_device", ] diff --git a/src/array_api_extra/_lib/_utils/_compat.pyi b/src/array_api_extra/_lib/_utils/_compat.pyi index f40d7556..48addda4 100644 --- a/src/array_api_extra/_lib/_utils/_compat.pyi +++ b/src/array_api_extra/_lib/_utils/_compat.pyi @@ -4,6 +4,7 @@ from __future__ import annotations from types import ModuleType +from typing import Any, TypeGuard # TODO import from typing (requires Python >=3.13) from typing_extensions import TypeIs @@ -12,29 +13,33 @@ from ._typing import Array, Device # pylint: disable=missing-class-docstring,unused-argument -class Namespace(ModuleType): - def device(self, x: Array, /) -> Device: ... - def array_namespace( *xs: Array | complex | None, api_version: str | None = None, use_compat: bool | None = None, -) -> Namespace: ... +) -> ModuleType: ... def device(x: Array, /) -> Device: ... def is_array_api_obj(x: object, /) -> TypeIs[Array]: ... -def is_array_api_strict_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_cupy_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_dask_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_jax_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_numpy_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_pydata_sparse_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_torch_namespace(xp: ModuleType, /) -> TypeIs[Namespace]: ... -def is_cupy_array(x: object, /) -> TypeIs[Array]: ... -def is_dask_array(x: object, /) -> TypeIs[Array]: ... -def is_jax_array(x: object, /) -> TypeIs[Array]: ... -def is_numpy_array(x: object, /) -> TypeIs[Array]: ... -def is_pydata_sparse_array(x: object, /) -> TypeIs[Array]: ... -def is_torch_array(x: object, /) -> TypeIs[Array]: ... -def is_lazy_array(x: object, /) -> TypeIs[Array]: ... -def is_writeable_array(x: object, /) -> TypeIs[Array]: ... +def is_array_api_strict_namespace(xp: ModuleType, /) -> bool: ... +def is_cupy_namespace(xp: ModuleType, /) -> bool: ... +def is_dask_namespace(xp: ModuleType, /) -> bool: ... +def is_jax_namespace(xp: ModuleType, /) -> bool: ... +def is_numpy_namespace(xp: ModuleType, /) -> bool: ... +def is_pydata_sparse_namespace(xp: ModuleType, /) -> bool: ... +def is_torch_namespace(xp: ModuleType, /) -> bool: ... +def is_cupy_array(x: object, /) -> TypeGuard[Array]: ... +def is_dask_array(x: object, /) -> TypeGuard[Array]: ... +def is_jax_array(x: object, /) -> TypeGuard[Array]: ... +def is_numpy_array(x: object, /) -> TypeGuard[Array]: ... +def is_pydata_sparse_array(x: object, /) -> TypeGuard[Array]: ... +def is_torch_array(x: object, /) -> TypeGuard[Array]: ... +def is_lazy_array(x: object, /) -> TypeGuard[Array]: ... +def is_writeable_array(x: object, /) -> TypeGuard[Array]: ... def size(x: Array, /) -> int | None: ... +def to_device( # type: ignore[explicit-any] + x: Array, + device: Device, # pylint: disable=redefined-outer-name + /, + *, + stream: int | Any | None = None, +) -> Array: ... diff --git a/vendor_tests/test_vendor.py b/vendor_tests/test_vendor.py index 4613edc7..374cba11 100644 --- a/vendor_tests/test_vendor.py +++ b/vendor_tests/test_vendor.py @@ -23,11 +23,12 @@ def test_vendor_compat(): is_torch_namespace, is_writeable_array, size, + to_device, ) x = xp.asarray([1, 2, 3]) assert array_namespace(x) is xp - device(x) + to_device(x, device(x)) assert is_array_api_obj(x) assert is_array_api_strict_namespace(xp) assert not is_cupy_array(x) From 483a16b9a187450d796b7515b62c575a179ba8ba Mon Sep 17 00:00:00 2001 From: crusaderky Date: Fri, 25 Apr 2025 16:08:26 +0100 Subject: [PATCH 17/17] Rework prepare_for_test --- src/array_api_extra/_lib/_testing.py | 80 +++++++++++++++------------- tests/test_testing.py | 9 +++- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/src/array_api_extra/_lib/_testing.py b/src/array_api_extra/_lib/_testing.py index c7f700ed..698d5948 100644 --- a/src/array_api_extra/_lib/_testing.py +++ b/src/array_api_extra/_lib/_testing.py @@ -7,7 +7,7 @@ import math from types import ModuleType -from typing import cast +from typing import Any, cast import numpy as np import pytest @@ -17,13 +17,15 @@ is_array_api_strict_namespace, is_cupy_namespace, is_dask_namespace, + is_jax_namespace, is_numpy_namespace, is_pydata_sparse_namespace, is_torch_namespace, + to_device, ) -from ._utils._typing import Array +from ._utils._typing import Array, Device -__all__ = ["xp_assert_close", "xp_assert_equal"] +__all__ = ["as_numpy_array", "xp_assert_close", "xp_assert_equal", "xp_assert_less"] def _check_ns_shape_dtype( @@ -81,23 +83,28 @@ def _check_ns_shape_dtype( return desired_xp -def _prepare_for_test(array: Array, xp: ModuleType) -> Array: +def as_numpy_array(array: Array, *, xp: ModuleType) -> np.typing.NDArray[Any]: # type: ignore[explicit-any] """ - Ensure that the array can be compared with np.testing. - - This involves transferring it from GPU to CPU memory, densifying it, etc. + Convert array to NumPy, bypassing GPU-CPU transfer guards and densification guards. """ - if is_torch_namespace(xp): - return np.asarray(array.cpu()) # type: ignore[attr-defined, return-value] # pyright: ignore[reportAttributeAccessIssue, reportUnknownArgumentType, reportReturnType] + if is_cupy_namespace(xp): + return xp.asnumpy(array) if is_pydata_sparse_namespace(xp): return array.todense() # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + + if is_torch_namespace(xp): + array = to_device(array, "cpu") if is_array_api_strict_namespace(xp): - # Note: we deliberately did not add a `.to_device` method in _typing.pyi - # even if it is required by the standard as many backends don't support it - return array.to_device(xp.Device("CPU_DEVICE")) # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] - if is_cupy_namespace(xp): - return xp.asnumpy(array) - return array + cpu: Device = xp.Device("CPU_DEVICE") + array = to_device(array, cpu) + if is_jax_namespace(xp): + import jax + + # Note: only needed if the transfer guard is enabled + cpu = cast(Device, jax.devices("cpu")[0]) + array = to_device(array, cpu) + + return np.asarray(array) def xp_assert_equal( @@ -132,9 +139,9 @@ def xp_assert_equal( numpy.testing.assert_array_equal : Similar function for NumPy arrays. """ xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) - actual = _prepare_for_test(actual, xp) - desired = _prepare_for_test(desired, xp) - np.testing.assert_array_equal(actual, desired, err_msg=err_msg) + actual_np = as_numpy_array(actual, xp=xp) + desired_np = as_numpy_array(desired, xp=xp) + np.testing.assert_array_equal(actual_np, desired_np, err_msg=err_msg) def xp_assert_less( @@ -167,9 +174,9 @@ def xp_assert_less( numpy.testing.assert_array_equal : Similar function for NumPy arrays. """ xp = _check_ns_shape_dtype(x, y, check_dtype, check_shape, check_scalar) - x = _prepare_for_test(x, xp) - y = _prepare_for_test(y, xp) - np.testing.assert_array_less(x, y, err_msg=err_msg) # type: ignore[call-overload] + x_np = as_numpy_array(x, xp=xp) + y_np = as_numpy_array(y, xp=xp) + np.testing.assert_array_less(x_np, y_np, err_msg=err_msg) def xp_assert_close( @@ -216,23 +223,22 @@ def xp_assert_close( """ xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) - floating = xp.isdtype(actual.dtype, ("real floating", "complex floating")) - if rtol is None and floating: - # multiplier of 4 is used as for `np.float64` this puts the default `rtol` - # roughly half way between sqrt(eps) and the default for - # `numpy.testing.assert_allclose`, 1e-7 - rtol = xp.finfo(actual.dtype).eps ** 0.5 * 4 - elif rtol is None: - rtol = 1e-7 - - actual = _prepare_for_test(actual, xp) - desired = _prepare_for_test(desired, xp) - + if rtol is None: + if xp.isdtype(actual.dtype, ("real floating", "complex floating")): + # multiplier of 4 is used as for `np.float64` this puts the default `rtol` + # roughly half way between sqrt(eps) and the default for + # `numpy.testing.assert_allclose`, 1e-7 + rtol = xp.finfo(actual.dtype).eps ** 0.5 * 4 + else: + rtol = 1e-7 + + actual_np = as_numpy_array(actual, xp=xp) + desired_np = as_numpy_array(desired, xp=xp) # JAX/Dask arrays work directly with `np.testing` - np.testing.assert_allclose( # type: ignore[call-overload] # pyright: ignore[reportCallIssue] - actual, # pyright: ignore[reportArgumentType] - desired, # pyright: ignore[reportArgumentType] - rtol=rtol, + np.testing.assert_allclose( # pyright: ignore[reportCallIssue] + actual_np, + desired_np, + rtol=rtol, # pyright: ignore[reportArgumentType] atol=atol, err_msg=err_msg, ) diff --git a/tests/test_testing.py b/tests/test_testing.py index 22291b65..97585c96 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,6 +8,7 @@ from array_api_extra._lib._backends import Backend from array_api_extra._lib._testing import ( + as_numpy_array, xp_assert_close, xp_assert_equal, xp_assert_less, @@ -17,7 +18,7 @@ is_dask_namespace, is_jax_namespace, ) -from array_api_extra._lib._utils._typing import Array +from array_api_extra._lib._utils._typing import Array, Device from array_api_extra.testing import lazy_xp_function # mypy: disable-error-code=decorated-any @@ -38,6 +39,12 @@ ) +def test_as_numpy_array(xp: ModuleType, device: Device): + x = xp.asarray([1, 2, 3], device=device) + y = as_numpy_array(x, xp=xp) + assert isinstance(y, np.ndarray) + + @pytest.mark.xfail_xp_backend(Backend.SPARSE, reason="no isdtype", strict=False) @pytest.mark.parametrize("func", [xp_assert_equal, xp_assert_close]) def test_assert_close_equal_basic(xp: ModuleType, func: Callable[..., None]): # type: ignore[explicit-any]