From e5bfb4b4cedc4591922a7c24b5a992328da1be26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 07:28:02 +0200 Subject: [PATCH 01/14] FIX Fix OpenBLAS detection for conda package on Windows --- tests/test_threadpoolctl.py | 9 +++++++++ threadpoolctl.py | 1 + 2 files changed, 10 insertions(+) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 541662a6..5a9385b9 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -913,3 +913,12 @@ def num_threads_created(limit: int) -> int: nmc_1 = num_threads_created(1) nmc_4 = num_threads_created(4) assert nmc_4 - nmc_1 == 6 + + +@pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) +def test_openblas_detection_after_import(module): + info = threadpool_info_from_subprocess(module) + + blas_info = select(info, internal_api="openblas") + assert len(blas_info) > 0 + diff --git a/threadpoolctl.py b/threadpoolctl.py index 72fda34f..5f54ec1c 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -270,6 +270,7 @@ class OpenBLASController(LibController): "libopenblas", "libblas", # legacy conda-forge Windows shim, see _make_controller_from_path "libscipy_openblas", + "openblas", # Windows conda package use openblas.dll ) _symbol_prefixes = ("", "scipy_") From 81439fd9bb4e87643cda5e5a6626feacd5011770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 07:30:22 +0200 Subject: [PATCH 02/14] Add changelog --- CHANGES.md | 3 +++ threadpoolctl.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ce3c8a39..c47d132b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -31,6 +31,9 @@ depend on how OpenBLAS was compiled with OpenMP. https://github.com/joblib/threadpoolctl/pull/228 +- Fix OpenBLAS detection for conda package on Windows + https://github.com/joblib/threadpoolctl/pull/240 + 3.6.0 (2025-03-13) ================== diff --git a/threadpoolctl.py b/threadpoolctl.py index 5f54ec1c..15146265 100644 --- a/threadpoolctl.py +++ b/threadpoolctl.py @@ -270,7 +270,7 @@ class OpenBLASController(LibController): "libopenblas", "libblas", # legacy conda-forge Windows shim, see _make_controller_from_path "libscipy_openblas", - "openblas", # Windows conda package use openblas.dll + "openblas", # Windows conda package use openblas.dll ) _symbol_prefixes = ("", "scipy_") From 20af1a208a26fbfd01fa62ee5d8779a4cca491df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 07:50:00 +0200 Subject: [PATCH 03/14] fix test to be generic with any BLAS implementation --- tests/test_threadpoolctl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 5a9385b9..ec65c3ef 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -916,9 +916,9 @@ def num_threads_created(limit: int) -> int: @pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) -def test_openblas_detection_after_import(module): +def test_blas_detection_after_import(module): info = threadpool_info_from_subprocess(module) - blas_info = select(info, internal_api="openblas") + blas_info = select(info, user_api="blas") assert len(blas_info) > 0 From 1006a4e8413c162b1410e44b9fd649553515bdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 07:51:00 +0200 Subject: [PATCH 04/14] black --- tests/test_threadpoolctl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index ec65c3ef..a82304e7 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -921,4 +921,3 @@ def test_blas_detection_after_import(module): blas_info = select(info, user_api="blas") assert len(blas_info) > 0 - From 8490a53d80dc73bf669f83d132de076c6b514c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 07:58:39 +0200 Subject: [PATCH 05/14] Fix atlas on CI --- tests/test_threadpoolctl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index a82304e7..bc8e6ae3 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -915,6 +915,7 @@ def num_threads_created(limit: int) -> int: assert nmc_4 - nmc_1 == 6 +@pytest.mark.skipif("atlas" in os.getenv("APT_BLAS", ""), reason="BLAS not detected with atlas") @pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) def test_blas_detection_after_import(module): info = threadpool_info_from_subprocess(module) From 2b0bdc086306a353ca691bf363eff692f0c24096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 09:50:44 +0200 Subject: [PATCH 06/14] black --- tests/test_threadpoolctl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index bc8e6ae3..1f348f91 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -915,7 +915,9 @@ def num_threads_created(limit: int) -> int: assert nmc_4 - nmc_1 == 6 -@pytest.mark.skipif("atlas" in os.getenv("APT_BLAS", ""), reason="BLAS not detected with atlas") +@pytest.mark.skipif( + "atlas" in os.getenv("APT_BLAS", ""), reason="BLAS not detected with atlas" +) @pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) def test_blas_detection_after_import(module): info = threadpool_info_from_subprocess(module) From d5f3ddde7726f285ede31052230e10fb7a342676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 11:31:14 +0200 Subject: [PATCH 07/14] Skip if numpy/scipy are not installed --- tests/test_threadpoolctl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 1f348f91..65903fd8 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -920,6 +920,8 @@ def num_threads_created(limit: int) -> int: ) @pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) def test_blas_detection_after_import(module): + pytest.importorskip(module) + info = threadpool_info_from_subprocess(module) blas_info = select(info, user_api="blas") From ab737d37f34dd4a73c6d575f7b5c10b0826c0d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 12:20:33 +0200 Subject: [PATCH 08/14] Make test conda-specific to simplify things --- tests/test_threadpoolctl.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 65903fd8..11b77924 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -915,14 +915,21 @@ def num_threads_created(limit: int) -> int: assert nmc_4 - nmc_1 == 6 -@pytest.mark.skipif( - "atlas" in os.getenv("APT_BLAS", ""), reason="BLAS not detected with atlas" -) +@pytest.mark.skipif(os.getenv("CONDA_PREFIX") is None, reason="conda-specific test") @pytest.mark.parametrize("module", ["numpy", "scipy.linalg"]) -def test_blas_detection_after_import(module): +def test_conda_blas_detection_after_import(module): pytest.importorskip(module) + conda_list_output = subprocess.check_output( + ["conda", "list", "--json"], text=True + ) + conda_list_items = json.loads(conda_list_output) + blas_names_from_conda = [each["name"] for each in conda_list_items if "openblas" in each["name"] or "mkl" in each["name"]] + blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda] info = threadpool_info_from_subprocess(module) blas_info = select(info, user_api="blas") assert len(blas_info) > 0 + + blas_names_from_threadpoolctl = [each["internal_api"] for each in blas_info] + assert set(blas_names_from_threadpoolctl).issubset(blas_names_from_conda) From 0327472b8dce2c3f9a55f111d84513308a041fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 12:21:13 +0200 Subject: [PATCH 09/14] black --- tests/test_threadpoolctl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 11b77924..b889101e 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -920,11 +920,13 @@ def num_threads_created(limit: int) -> int: def test_conda_blas_detection_after_import(module): pytest.importorskip(module) - conda_list_output = subprocess.check_output( - ["conda", "list", "--json"], text=True - ) + conda_list_output = subprocess.check_output(["conda", "list", "--json"], text=True) conda_list_items = json.loads(conda_list_output) - blas_names_from_conda = [each["name"] for each in conda_list_items if "openblas" in each["name"] or "mkl" in each["name"]] + blas_names_from_conda = [ + each["name"] + for each in conda_list_items + if "openblas" in each["name"] or "mkl" in each["name"] + ] blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda] info = threadpool_info_from_subprocess(module) From c193629f47da97106e9f45394ad0f3818216209b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 14:03:07 +0200 Subject: [PATCH 10/14] Tweak test --- tests/test_threadpoolctl.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index b889101e..d2d8a53d 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -920,18 +920,30 @@ def num_threads_created(limit: int) -> int: def test_conda_blas_detection_after_import(module): pytest.importorskip(module) - conda_list_output = subprocess.check_output(["conda", "list", "--json"], text=True) + info = threadpool_info_from_subprocess(module) + + conda = which("conda") or which("mamba") or which("micromamba") + conda_list_output = subprocess.check_output([conda, "list", "--json"], text=True) conda_list_items = json.loads(conda_list_output) blas_names_from_conda = [ each["name"] for each in conda_list_items - if "openblas" in each["name"] or "mkl" in each["name"] + if any( + blas_lib in each["name"] for blas_lib in ["accelerate", "openblas", "mkl"] + ) ] blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda] - info = threadpool_info_from_subprocess(module) + + if "accelerate" in blas_names_from_conda: + pytest.skip("threadpoolctl does not know how to inspect Accelerate") + return blas_info = select(info, user_api="blas") assert len(blas_info) > 0 + if not blas_names_from_conda: + # 'module' has been installed with pip + return + blas_names_from_threadpoolctl = [each["internal_api"] for each in blas_info] assert set(blas_names_from_threadpoolctl).issubset(blas_names_from_conda) From ea1648e2d2307d87bfb60d1cd747f4dbb69ad6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 14:13:44 +0200 Subject: [PATCH 11/14] Fix windows --- tests/test_threadpoolctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index d2d8a53d..3db44fd8 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -922,7 +922,7 @@ def test_conda_blas_detection_after_import(module): info = threadpool_info_from_subprocess(module) - conda = which("conda") or which("mamba") or which("micromamba") + conda = which("conda") or which("micromamba") or which("mamba") conda_list_output = subprocess.check_output([conda, "list", "--json"], text=True) conda_list_items = json.loads(conda_list_output) blas_names_from_conda = [ From c8a66613dd6f2284d51cc0282cc1b8aa125cee45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 14:46:45 +0200 Subject: [PATCH 12/14] Fix test --- tests/test_threadpoolctl.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 3db44fd8..4eb3f097 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -929,21 +929,21 @@ def test_conda_blas_detection_after_import(module): each["name"] for each in conda_list_items if any( - blas_lib in each["name"] for blas_lib in ["accelerate", "openblas", "mkl"] + blas_lib in each["name"] for blas_lib in ["openblas", "mkl"] ) ] blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda] - if "accelerate" in blas_names_from_conda: + if "accelerate" in conda_list_output: pytest.skip("threadpoolctl does not know how to inspect Accelerate") - return + + if not blas_names_from_conda: + pytest.skip( + f"{module} has been installed with pip, this is a conda-specific test" + ) blas_info = select(info, user_api="blas") assert len(blas_info) > 0 - if not blas_names_from_conda: - # 'module' has been installed with pip - return - blas_names_from_threadpoolctl = [each["internal_api"] for each in blas_info] assert set(blas_names_from_threadpoolctl).issubset(blas_names_from_conda) From b87cd2758bf10684837fca0a0119a95caafd7c6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Est=C3=A8ve?= Date: Tue, 8 Sep 2026 14:56:34 +0200 Subject: [PATCH 13/14] black --- tests/test_threadpoolctl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index 4eb3f097..aed987c2 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -928,9 +928,7 @@ def test_conda_blas_detection_after_import(module): blas_names_from_conda = [ each["name"] for each in conda_list_items - if any( - blas_lib in each["name"] for blas_lib in ["openblas", "mkl"] - ) + if any(blas_lib in each["name"] for blas_lib in ["openblas", "mkl"]) ] blas_names_from_conda = [each.replace("lib", "") for each in blas_names_from_conda] From c8639901332819b2fdba30d8116388e3fc7df025 Mon Sep 17 00:00:00 2001 From: Olivier Grisel Date: Tue, 8 Sep 2026 16:37:56 +0200 Subject: [PATCH 14/14] Ignore flexiblas in the new test --- tests/test_threadpoolctl.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_threadpoolctl.py b/tests/test_threadpoolctl.py index aed987c2..9f60cbba 100644 --- a/tests/test_threadpoolctl.py +++ b/tests/test_threadpoolctl.py @@ -943,5 +943,11 @@ def test_conda_blas_detection_after_import(module): blas_info = select(info, user_api="blas") assert len(blas_info) > 0 - blas_names_from_threadpoolctl = [each["internal_api"] for each in blas_info] + # Flexiblas is built from source on our CI. At the time of writing, it is not + # available in the conda-forge channel. + blas_names_from_threadpoolctl = [ + each["internal_api"] + for each in blas_info + if each["internal_api"] != "flexiblas" + ] assert set(blas_names_from_threadpoolctl).issubset(blas_names_from_conda)