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: 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 diff --git a/pixi.lock b/pixi.lock index 56ee7c56..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: 74777bddfe6ab8d3ced9e5d1c645cb95c637707a45de9e96c88fc3b41723e3af + 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 67651904..cba9c4cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,17 +54,17 @@ 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" -basedpyright = ">=1.28.3" +basedpyright = ">=1.28.5" numpydoc = ">=1.8.0,<2" # import dependencies for mypy: array-api-strict = ">=2.3.1" numpy = ">=2.1.3" pytest = ">=8.3.5" -hypothesis = ">=6.130.11" +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.130.11" +hypothesis = ">=6.131.8" array-api-strict = ">=2.3.1" numpy = ">=1.22.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] @@ -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] @@ -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/renovate.json b/renovate.json index 10ea8ab9..05342e62 100644 --- a/renovate.json +++ b/renovate.json @@ -50,6 +50,22 @@ "matchManagers": ["github-actions"], "matchPackageNames": ["python"], "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 * *"] } ] } diff --git a/src/array_api_extra/_lib/_testing.py b/src/array_api_extra/_lib/_testing.py index 319297c8..698d5948 100644 --- a/src/array_api_extra/_lib/_testing.py +++ b/src/array_api_extra/_lib/_testing.py @@ -7,8 +7,9 @@ import math from types import ModuleType -from typing import cast +from typing import Any, cast +import numpy as np import pytest from ._utils._compat import ( @@ -16,16 +17,23 @@ 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( - 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 +55,67 @@ 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) == np.isscalar(desired), _msg 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 xp.testing or 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 array.cpu() # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue] + 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] - # Note: nothing to do for CuPy, because it uses a bespoke test function - 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(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 +127,56 @@ 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) - actual = _prepare_for_test(actual, xp) - desired = _prepare_for_test(desired, xp) + xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) + 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) - 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) +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_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( @@ -132,6 +186,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 +205,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,43 +221,32 @@ def xp_assert_close( ----- The default `atol` and `rtol` differ from `xp.all(xpx.isclose(a, b))`. """ - xp = _check_ns_shape_dtype(actual, desired) - - 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 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, - ) - - -def xfail(request: pytest.FixtureRequest, reason: str) -> None: + xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar) + + 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( # pyright: ignore[reportCallIssue] + actual_np, + desired_np, + rtol=rtol, # pyright: ignore[reportArgumentType] + atol=atol, + err_msg=err_msg, + ) + + +def xfail( + request: pytest.FixtureRequest, *, reason: str, strict: bool | None = None +) -> None: """ XFAIL the currently running test. @@ -209,5 +260,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/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/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/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..0cee0b4d 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 @@ -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)) 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 diff --git a/tests/test_testing.py b/tests/test_testing.py index ff67121b..97585c96 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 @@ -6,13 +7,18 @@ 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 ( + as_numpy_array, + xp_assert_close, + xp_assert_equal, + xp_assert_less, +) from array_api_extra._lib._utils._compat import ( array_namespace, 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 @@ -22,15 +28,25 @@ "func", [ xp_assert_equal, + xp_assert_less, 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 + ), ), ], ) -@param_assert_equal_close +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] func(xp.asarray(0), xp.asarray(0)) func(xp.asarray([1, 2]), xp.asarray([1, 2])) @@ -50,8 +66,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"): @@ -60,6 +76,58 @@ 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_less_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([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_less_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(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, xp_assert_less]) +@pytest.mark.parametrize("check_scalar", [False, True]) +def test_assert_close_equal_less_scalar( # type: ignore[explicit-any] + xp: ModuleType, + 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(xp.nan), np.asarray(xp.nan)[()], 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) @@ -71,9 +139,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. @@ -130,13 +207,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 +229,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: 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)