diff --git a/pixi.lock b/pixi.lock index 3bdd8128..e9681410 100644 --- a/pixi.lock +++ b/pixi.lock @@ -756,6 +756,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/roman-numerals-py-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/win-64/ruff-0.14.10-h37e10c4_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda @@ -1531,6 +1532,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/roman-numerals-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/roman-numerals-py-4.1.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/win-64/ruff-0.14.10-h37e10c4_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/snowballstemmer-3.0.1-pyhd8ed1ab_0.conda @@ -2478,6 +2480,7 @@ environments: - conda: https://prefix.dev/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py313h4b8bb8b_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda @@ -2525,6 +2528,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py313hefbb9bc_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda @@ -2571,6 +2575,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py313h29d7d31_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda @@ -2616,6 +2621,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - conda: https://prefix.dev/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda @@ -2992,6 +2998,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/win-64/pytorch-2.9.1-cpu_mkl_py313_h4c75245_101.conda - conda: https://prefix.dev/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -3374,6 +3381,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/win-64/pytorch-2.9.1-cpu_mkl_py311_h668fc7c_101.conda - conda: https://prefix.dev/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py311h9c22a71_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -3826,6 +3834,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/win-64/pytorch-2.9.1-cuda128_mkl_py313_h7f80487_300.conda - conda: https://prefix.dev/conda-forge/win-64/pyyaml-6.0.3-py313hd650c13_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -4276,6 +4285,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/win-64/pytorch-2.9.1-cuda128_mkl_py311_h7c65ee9_300.conda - conda: https://prefix.dev/conda-forge/win-64/pyyaml-6.0.3-py311h3f79411_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py311h9c22a71_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/win-64/sleef-3.9.0-h67fd636_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -4357,6 +4367,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313t.conda - conda: https://prefix.dev/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py313h0dc34c3_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda @@ -4418,6 +4429,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313t.conda - conda: https://prefix.dev/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py313h6f07835_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda @@ -4477,6 +4489,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313t.conda - conda: https://prefix.dev/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py313h0628c33_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda @@ -4535,6 +4548,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/python-freethreading-3.13.11-h92d6c8b_0.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313t.conda - conda: https://prefix.dev/conda-forge/noarch/pyyaml-6.0.3-pyh7db6752_0.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313hff732fb_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda @@ -4604,6 +4618,7 @@ environments: - conda: https://prefix.dev/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.15.2-py311h8f841c2_0.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda @@ -4651,6 +4666,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-64/python-3.11.14-h74c2667_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.15.2-py311h0c91ca8_0.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda @@ -4696,6 +4712,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.15.2-py311h0675101_0.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda @@ -4740,6 +4757,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - conda: https://prefix.dev/conda-forge/win-64/python-3.11.14-h0159041_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.15.2-py311h99d06ae_0.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda @@ -4806,6 +4824,7 @@ environments: - conda: https://prefix.dev/conda-forge/linux-64/python-3.11.14-hd63d673_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py311hbe70eeb_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda @@ -4853,6 +4872,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-64/python-3.11.14-h74c2667_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py311hd77d3c2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda @@ -4898,6 +4918,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.11.14-h18782d2_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py311ha71c161_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda @@ -4942,6 +4963,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - conda: https://prefix.dev/conda-forge/win-64/python-3.11.14-h0159041_2_cpython.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.11-8_cp311.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py311h9c22a71_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda @@ -5004,6 +5026,7 @@ environments: - conda: https://prefix.dev/conda-forge/linux-64/python-3.13.11-hc97d973_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py313h4b8bb8b_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/linux-64/tk-8.6.13-noxft_ha0e22de_103.conda @@ -5051,6 +5074,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-64/python-3.13.11-h17c18a5_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/osx-64/readline-8.3-h68b038d_0.conda + - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py313hefbb9bc_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-64/tk-8.6.13-hf689a15_3.conda @@ -5097,6 +5121,7 @@ environments: - conda: https://prefix.dev/conda-forge/osx-arm64/python-3.13.11-hfc2f54d_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://prefix.dev/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py313h29d7d31_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_3.conda @@ -5142,6 +5167,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda - conda: https://prefix.dev/conda-forge/win-64/python-3.13.11-h09917c8_100_cp313.conda - conda: https://prefix.dev/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda - conda: https://prefix.dev/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/win-64/tbb-2022.3.0-hd094cb3_1.conda @@ -5296,7 +5322,7 @@ packages: - pypi: ./ name: array-api-extra version: 0.10.0.dev0 - sha256: dbc3abd6bb4ce3b508394e29344b9fd75c70414529dcaede915511a9cb0607c5 + sha256: b7af66e75c59d1a6eaa343bc80d5227d257885c8b3aaf3e863062bb18c031972 requires_dist: - array-api-compat>=1.12.0,<2 requires_python: '>=3.11' @@ -12941,6 +12967,29 @@ packages: - pkg:pypi/ruff?source=compressed-mapping size: 11908812 timestamp: 1766095035171 +- conda: https://prefix.dev/conda-forge/linux-64/scipy-1.15.2-py311h8f841c2_0.conda + sha256: 6d0902775e3ff96dd1d36ac627e03fe6c0b3d2159bb71e115dd16a1f31693b25 + md5: 5ec0a1732a05376241e1e4c6d50e0e91 + depends: + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - libstdcxx >=13 + - numpy <2.5 + - numpy >=1.19,<3 + - numpy >=1.23.5 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 17193126 + timestamp: 1739791897768 - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py311hbe70eeb_2.conda sha256: a13084f1556674ea74de2ecbe50333d938dab8ef27f536408592ba312363c400 md5: 1f9587850322d7d77ea14d4fee3d16d8 @@ -12964,6 +13013,29 @@ packages: - pkg:pypi/scipy?source=hash-mapping size: 17026343 timestamp: 1766108701646 +- conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py313h0dc34c3_2.conda + sha256: 9496a2040becb769af13d1f1709e23fae0c4c8d143f00ca6fdf2856e01007d8b + md5: 44c5b351e76d345ea2b86876dc517b59 + depends: + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - libstdcxx >=14 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313t + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 16836737 + timestamp: 1766108883881 - conda: https://prefix.dev/conda-forge/linux-64/scipy-1.16.3-py313h4b8bb8b_2.conda sha256: a5ddc728be0589e770f59e45e3c6c670c56d96a801ddf76a304cc0af7bcef5c4 md5: 0be9bd58abfb3e8f97260bd0176d5331 @@ -12987,6 +13059,28 @@ packages: - pkg:pypi/scipy?source=compressed-mapping size: 16785487 timestamp: 1766108773270 +- conda: https://prefix.dev/conda-forge/osx-64/scipy-1.15.2-py311h0c91ca8_0.conda + sha256: 796252d7772df42edd29a45ae70eb18843a7e476d42c96c273cd6e677ec148c8 + md5: 58c17d411ed0cd1220ed3e824a3efc82 + depends: + - __osx >=10.13 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.5 + - numpy >=1.19,<3 + - numpy >=1.23.5 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15759628 + timestamp: 1739792317052 - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py311hd77d3c2_2.conda sha256: dee00542bb0aed60d6160883365c19317fe63b34ea0f3237a8725fee297341f6 md5: 5153a584333b37a433b5e1244606ef3d @@ -13009,6 +13103,28 @@ packages: - pkg:pypi/scipy?source=hash-mapping size: 15290138 timestamp: 1766108637965 +- conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py313h6f07835_2.conda + sha256: d5f3bcbdacfdd86569910717f8b98e43c1361105c894b2a75a431116292481d3 + md5: d7320342f63c804324931942facb178a + depends: + - __osx >=10.13 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313t + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15320844 + timestamp: 1766108736410 - conda: https://prefix.dev/conda-forge/osx-64/scipy-1.16.3-py313hefbb9bc_2.conda sha256: bb73a8bf8598537e25d6e81c05f607b4798597824c4fbfa876aeee3d2447d07e md5: 11104881493e37e12558eeb97193ad08 @@ -13031,6 +13147,29 @@ packages: - pkg:pypi/scipy?source=hash-mapping size: 15284100 timestamp: 1766108740047 +- conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.15.2-py311h0675101_0.conda + sha256: bc3e873e85c55deaaad446c410d9001d12a133c1b48fa2cb0050b4f46f926aa3 + md5: df904770f3fdb6c0265a09cdc22acf54 + depends: + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.5 + - numpy >=1.19,<3 + - numpy >=1.23.5 + - python >=3.11,<3.12.0a0 + - python >=3.11,<3.12.0a0 *_cpython + - python_abi 3.11.* *_cp311 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 14569129 + timestamp: 1739792318601 - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py311ha71c161_2.conda sha256: 3a3bd525f126e7414c07bcf915060a36bfc3dac4cf31335585be942128d4337f md5: 1283a5d5d1c10e981638d2bd02c4eac6 @@ -13054,6 +13193,29 @@ packages: - pkg:pypi/scipy?source=hash-mapping size: 13815921 timestamp: 1766108875815 +- conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py313h0628c33_2.conda + sha256: 042a5bbc6aad52cec0ae0b5a3372a6413aac563502534ec380ec6e11798ea385 + md5: 656cdfff488a825e6da6b214fa30ff4a + depends: + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=19 + - libgfortran + - libgfortran5 >=14.3.0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.13,<3.14.0a0 + - python >=3.13,<3.14.0a0 *_cp313t + - python_abi 3.13.* *_cp313t + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 14133337 + timestamp: 1766109671329 - conda: https://prefix.dev/conda-forge/osx-arm64/scipy-1.16.3-py313h29d7d31_2.conda sha256: ee3cbddb7d598c78b592fafbfa3eaf8c89df353bbed56a1a9f32e9f7daa49bb4 md5: a3324bd937a39cbbf1cbe0940160e19e @@ -13077,6 +13239,90 @@ packages: - pkg:pypi/scipy?source=hash-mapping size: 13929516 timestamp: 1766109298759 +- conda: https://prefix.dev/conda-forge/win-64/scipy-1.15.2-py311h99d06ae_0.conda + sha256: 62ae1a1e02c919513213351474d1c72480fb70388a345fa81f1c95fa822d98bf + md5: c7ec15b5ea6a27bb71af2ea5f7c97cbb + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.5 + - numpy >=1.19,<3 + - numpy >=1.23.5 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15487645 + timestamp: 1739793313482 +- conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py311h9c22a71_2.conda + sha256: 49129601dc89d49742d342ace70f4ec0127a5eb24a50d66f95f91db01b3a23d5 + md5: 4b663de0f0c8ac0fbb4a4d9ee8536b0f + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.11,<3.12.0a0 + - python_abi 3.11.* *_cp311 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15129579 + timestamp: 1766109708812 +- conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313he51e9a2_2.conda + sha256: 997a2202126425438a16de7ef1e5e924bd66feb43bda5b71326e281c7331489d + md5: a49556572438d5477f1eca06bb6d0770 + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15066293 + timestamp: 1766109539389 +- conda: https://prefix.dev/conda-forge/win-64/scipy-1.16.3-py313hff732fb_2.conda + sha256: 4fcd05087ff6081304dc64472b94e388ca945e88174c95429757b0cdc0b7b2b9 + md5: e6f67e36e4103657c984321eaff84064 + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - numpy <2.6 + - numpy >=1.23,<3 + - numpy >=1.25.2 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313t + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/scipy?source=hash-mapping + size: 15263592 + timestamp: 1766109705205 - conda: https://prefix.dev/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863 md5: 4de79c071274a53dcaf2a8c749d1499e diff --git a/pyproject.toml b/pyproject.toml index d6f02182..3d619f3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ pytest-cov = ">=7.0.0" hypothesis = ">=6.148.7" array-api-strict = ">=2.4.1,<2.5" numpy = ">=1.22.0" +scipy = ">=1.15.2,<2" [tool.pixi.feature.tests.tasks] tests = { cmd = "pytest -v", description = "Run tests" } diff --git a/src/array_api_extra/testing.py b/src/array_api_extra/testing.py index d40fea1a..9f2b0d38 100644 --- a/src/array_api_extra/testing.py +++ b/src/array_api_extra/testing.py @@ -10,8 +10,9 @@ import enum import warnings from collections.abc import Callable, Generator, Iterator, Sequence -from functools import wraps -from types import ModuleType +from functools import update_wrapper, wraps +from inspect import getattr_static +from types import FunctionType, ModuleType from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast from ._lib._utils._compat import is_dask_namespace, is_jax_namespace @@ -48,8 +49,21 @@ class Deprecated(enum.Enum): DEPRECATED = Deprecated.DEPRECATED +def _clone_function(f: Callable[..., Any]) -> Callable[..., Any]: + """Returns a clone of an existing function.""" + f_new = FunctionType( + f.__code__, + f.__globals__, + name=f.__name__, + argdefs=f.__defaults__, + closure=f.__closure__, + ) + f_new.__kwdefaults__ = f.__kwdefaults__ + return update_wrapper(f_new, f) + + def lazy_xp_function( - func: Callable[..., Any], + func: Callable[..., Any] | tuple[type, str], *, allow_dask_compute: bool | int = False, jax_jit: bool = True, @@ -69,8 +83,9 @@ def lazy_xp_function( Parameters ---------- - func : callable - Function to be tested. + func : callable | tuple[type, str] + Function to be tested, or a tuple containing an (uninstantiated) class and a + method name to specify a class method to be tested. 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 @@ -204,15 +219,49 @@ def test_myfunc(xp): DeprecationWarning, stacklevel=2, ) - tags = { + tags: dict[str, bool | int | type] = { "allow_dask_compute": allow_dask_compute, "jax_jit": jax_jit, } + if isinstance(func, tuple): + # Replace the method with a clone before adding tags + # to avoid adding unwanted tags to a parent method when + # the method was inherited from a parent class. + # Note: can't just accept an unbound method `cls.method_name` because in + # case of inheritance it would be impossible to attribute it to the child class. + # This also makes it so tagged methods will appear in their class's ``__dict__`` + # and thus findable by ``iter_tagged_modules`` below. + cls, method_name = func + # The method might be a staticmethod or classmethod so we need to do a dance + # to ensure that this is preserved. + raw_attr = getattr_static(cls, method_name) + method = getattr(cls, method_name) + if isinstance(raw_attr, classmethod): + method = method.__func__ + cloned_method = _clone_function(method) + + method_to_set: Any + if isinstance(raw_attr, staticmethod): + method_to_set = staticmethod(cloned_method) + elif isinstance(raw_attr, classmethod): + method_to_set = classmethod(cloned_method) + else: + method_to_set = cloned_method + + setattr(cls, method_name, method_to_set) + f = getattr(cls, method_name) + if isinstance(raw_attr, classmethod): + f = f.__func__ + # Annotate that cls owns this method so we can check that later. + tags["owner"] = cls + else: + f = func + try: - func._lazy_xp_function = tags # type: ignore[attr-defined] # pylint: disable=protected-access # pyright: ignore[reportFunctionMemberAccess] + f._lazy_xp_function = tags # pylint: disable=protected-access # pyright: ignore[reportFunctionMemberAccess] except AttributeError: # @cython.vectorize - _ufuncs_tags[func] = tags + _ufuncs_tags[f] = tags def patch_lazy_xp_functions( @@ -224,10 +273,11 @@ def patch_lazy_xp_functions( """ Test lazy execution of functions tagged with :func:`lazy_xp_function`. - If ``xp==jax.numpy``, search for all functions which have been tagged with - :func:`lazy_xp_function` in the globals of the module that defines the current test, - as well as in the ``lazy_xp_modules`` list in the globals of the same module, - and wrap them with :func:`jax.jit`. Unwrap them at the end of the test. + If ``xp==jax.numpy``, search for all functions and methods which have been tagged + with :func:`lazy_xp_function` in the globals of the module that defines the current + test, as well as in the ``lazy_xp_modules`` list in the globals of the same module, + and wrap them with :func:`jax.jit`. + Unwrap them at the end of the test. If ``xp==dask.array``, wrap the functions with a decorator that disables ``compute()`` and ``persist()`` and ensures that exceptions and warnings are raised @@ -271,18 +321,34 @@ def xp(request): the example above. """ mod = cast(ModuleType, request.module) - mods = [mod, *cast(list[ModuleType], getattr(mod, "lazy_xp_modules", []))] - - to_revert: list[tuple[ModuleType, str, object]] = [] - - def temp_setattr(mod: ModuleType, name: str, func: object) -> None: + search_targets: list[ModuleType | type] = [ + mod, + *cast(list[ModuleType], getattr(mod, "lazy_xp_modules", [])), + ] + # Also search for classes within the above modules which have had lazy_xp_function + # applied to methods through ``lazy_xp_function((cls, method_name))`` syntax. + # We might end up adding classes incidentally imported into modules, so using a + # set here to cut down on potential redundancy. + classes: set[type] = set() + for target in search_targets: + for obj in target.__dict__.values(): + if isinstance(obj, type): + classes.add(obj) + search_targets.extend(classes) + + to_revert: list[tuple[ModuleType | type, str, object]] = [] + + def temp_setattr(target: ModuleType | type, name: str, func: object) -> None: """ Variant of monkeypatch.setattr, which allows monkey-patching only selected parameters of a test so that pytest-run-parallel can run on the remainder. """ - assert hasattr(mod, name) - to_revert.append((mod, name, getattr(mod, name))) - setattr(mod, name, func) + assert hasattr(target, name) + # Need getattr_static because the attr could be a staticmethod or other + # descriptor and we don't want that to be stripped away. + original = getattr_static(target, name) + to_revert.append((target, name, original)) + setattr(target, name, func) if monkeypatch is not None: warnings.warn( @@ -298,10 +364,19 @@ def temp_setattr(mod: ModuleType, name: str, func: object) -> None: temp_setattr = monkeypatch.setattr # type: ignore[assignment] # pyright: ignore[reportAssignmentType] def iter_tagged() -> Iterator[ - tuple[ModuleType, str, Callable[..., Any], dict[str, Any]] + tuple[ModuleType | type, str, Any, Callable[..., Any], dict[str, Any]] ]: - for mod in mods: - for name, func in mod.__dict__.items(): + for target in search_targets: + for name, attr in target.__dict__.items(): + # attr might be a staticmethod or classmethod. If so we need + # to peel it back and wrap the underlying function and later + # make sure not to accidentally replace it with a regular + # method. + func: Any = ( + attr.__func__ + if isinstance(attr, (staticmethod, classmethod)) + else attr + ) tags: dict[str, Any] | None = None with contextlib.suppress(AttributeError): tags = func._lazy_xp_function # pylint: disable=protected-access @@ -309,23 +384,49 @@ def iter_tagged() -> Iterator[ with contextlib.suppress(KeyError, TypeError): tags = _ufuncs_tags[func] if tags is not None: - yield mod, name, func, tags - + if isinstance(target, type) and tags.get("owner") is not target: + # There's a common pattern to wrap functions in namespace + # classes to bypass lazy_xp_function like this: + # + # class naked: + # myfunc = mymodule.myfunc + # + # To ensure this still works when checking for tags in + # attributes of classes, ensure that target is the actual + # owning class where func was defined. + continue + # put attr, and func in the outputs so we can later tell + # if this was a staticmethod or classmethod. + yield target, name, attr, func, tags + + wrapped: Any if is_dask_namespace(xp): - for mod, name, func, tags in iter_tagged(): + for target, name, attr, 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) - temp_setattr(mod, name, wrapped) + # If we're dealing with a staticmethod or classmethod, make + # sure things stay that way. + if isinstance(attr, staticmethod): + wrapped = staticmethod(wrapped) + elif isinstance(attr, classmethod): + wrapped = classmethod(wrapped) + temp_setattr(target, name, wrapped) elif is_jax_namespace(xp): - for mod, name, func, tags in iter_tagged(): + for target, name, attr, func, tags in iter_tagged(): if tags["jax_jit"]: wrapped = jax_autojit(func) - temp_setattr(mod, name, wrapped) + # If we're dealing with a staticmethod or classmethod, make + # sure things stay that way. + if isinstance(attr, staticmethod): + wrapped = staticmethod(wrapped) + elif isinstance(attr, classmethod): + wrapped = classmethod(wrapped) + temp_setattr(target, name, wrapped) # We can't just decorate patch_lazy_xp_functions with # @contextlib.contextmanager because it would not work with the @@ -335,8 +436,8 @@ def revert_on_exit() -> Generator[None]: try: yield finally: - for mod, name, orig_func in to_revert: - setattr(mod, name, orig_func) + for target, name, orig_func in to_revert: + setattr(target, name, orig_func) return revert_on_exit() diff --git a/tests/test_testing.py b/tests/test_testing.py index 7e72ffbf..b70de734 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,9 +1,10 @@ from collections.abc import Callable, Iterator from types import ModuleType -from typing import cast +from typing import Any, cast, final import numpy as np import pytest +from typing_extensions import override from array_api_extra._lib._backends import Backend from array_api_extra._lib._testing import ( @@ -321,6 +322,128 @@ def test_lazy_xp_function_cython_ufuncs(xp: ModuleType, library: Backend): xp_assert_equal(cast(Array, erf(x)), xp.asarray([1.0, 1.0])) +class A: + def __init__(self, x: Array): + xp = array_namespace(x) + self._xp: ModuleType = xp + self.x: Any = np.asarray(x) + + def f(self, y: Array) -> Array: + return self._xp.asarray(np.matmul(self.x, np.asarray(y))) + + def g(self, y: Array, z: Array) -> Array: + return self.f(y) + self.f(z) + + def h(self, y: Array) -> bool: + return bool(self._xp.any(y)) + + +class B(A): + @override + def __init__(self, x: Array): # pyright: ignore[reportMissingSuperCall] + xp = array_namespace(x) + self._xp: ModuleType = xp + self.x: Any = xp.asarray(x) + + @override + def f(self, y: Array) -> Array: + return self._xp.matmul(self.x, y) + + @staticmethod + def k(y: Array) -> "B": + return B(2.0 * y) + + @staticmethod + def j(y: Array) -> "B": + xp = array_namespace(y) + y = xp.asarray(y) + if bool(xp.any(y)): + return B(y) + return B(y + 1.0) + + @classmethod + def w(cls, y: Array) -> "B": + xp = array_namespace(y) + y = xp.asarray(y) + if bool(xp.any(y)): + return B(y) + return B(y + 1.0) + + +@final +class eager: + # this needs to be a staticmethod to appease the type checker + non_materializable5 = staticmethod(non_materializable5) + + +lazy_xp_function((B, "g")) +lazy_xp_function((B, "h")) +lazy_xp_function((B, "k")) +lazy_xp_function((B, "j")) +lazy_xp_function((B, "w")) + + +class TestLazyXpFunctionClasses: + def test_parent_method_not_tagged(self): + assert hasattr(B.g, "_lazy_xp_function") + assert not hasattr(A.g, "_lazy_xp_function") + + @pytest.mark.skip_xp_backend(Backend.SPARSE, reason="converts to NumPy") + @pytest.mark.skip_xp_backend(Backend.CUPY, reason="converts to NumPy") + @pytest.mark.skip_xp_backend(Backend.JAX_GPU, reason="converts to NumPy") + @pytest.mark.skip_xp_backend(Backend.TORCH_GPU, reason="converts to NumPy") + def test_lazy_xp_function_classes(self, xp: ModuleType, library: Backend): + x = xp.asarray([1.1, 2.2, 3.3]) + y = xp.asarray([1.0, 2.0, 3.0]) + foo = A(x) + bar = B(x) + + if library.like(Backend.JAX): + with pytest.raises( + TypeError, match="Attempted boolean conversion of traced array" + ): + assert bar.h(y) + + assert foo.h(y) + + def test_static_methods_preserved(self, xp: ModuleType): + # Tests that static methods stay static methods when + # lazy_xp_function is applied. + x = xp.asarray([1.1, 2.2, 3.3]) + foo = B(x) + bar = foo.k(x) + xp_assert_equal(bar.x, 2.0 * foo.x) + + @pytest.mark.skip_xp_backend(Backend.DASK, reason="calls dask.compute()") + def test_static_methods_wrapped(self, xp: ModuleType, library: Backend): + x = xp.asarray([1.1, 2.2, 3.3]) + foo = B(x) + + if library.like(Backend.JAX): + with pytest.raises( + TypeError, match="Attempted boolean conversion of traced array" + ): + assert isinstance(foo.j(x), B) + else: + assert isinstance(foo.j(x), B) + + @pytest.mark.skip_xp_backend(Backend.DASK, reason="calls dask.compute()") + def test_class_methods_wrapped(self, xp: ModuleType, library: Backend): + x = xp.asarray([1.1, 2.2, 3.3]) + if library.like(Backend.JAX): + with pytest.raises( + TypeError, match="Attempted boolean conversion of traced array" + ): + assert isinstance(B.w(x), B) + else: + assert isinstance(B.w(x), B) + + def test_circumvention(self, xp: ModuleType): + x = xp.asarray([1.0, 2.0]) + y = eager.non_materializable5(x) + xp_assert_equal(y, x) + + def dask_raises(x: Array) -> Array: def _raises(x: Array) -> Array: # Test that map_blocks doesn't eagerly call the function;