From f9498423141f59522c79145abdcc414924924929 Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:42:08 +0200 Subject: [PATCH 1/2] fix(discovery): reuse preferred paths across specifications --- .../+reuse-preferred-paths.bugfix.rst | 1 + src/python_discovery/_discovery.py | 3 ++- tests/test_discovery.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/+reuse-preferred-paths.bugfix.rst diff --git a/docs/changelog/+reuse-preferred-paths.bugfix.rst b/docs/changelog/+reuse-preferred-paths.bugfix.rst new file mode 100644 index 0000000..ccd2768 --- /dev/null +++ b/docs/changelog/+reuse-preferred-paths.bugfix.rst @@ -0,0 +1 @@ +Reuse preferred interpreter paths supplied as an iterator when trying multiple interpreter specifications. diff --git a/src/python_discovery/_discovery.py b/src/python_discovery/_discovery.py index 8511852..1263bd9 100644 --- a/src/python_discovery/_discovery.py +++ b/src/python_discovery/_discovery.py @@ -47,8 +47,9 @@ def get_interpreter( :return: the first matching interpreter, or ``None`` if no match is found. """ specs = [key] if isinstance(key, str) else key + first_with = tuple(try_first_with or ()) for spec_str in specs: - if result := _find_interpreter(spec_str, try_first_with or (), cache, env, predicate): + if result := _find_interpreter(spec_str, first_with, cache, env, predicate): return result return None diff --git a/tests/test_discovery.py b/tests/test_discovery.py index fd5338b..2bca3b1 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -133,6 +133,25 @@ def test_discovery_invalid_specifier_raises(session_cache: DiskCache) -> None: get_interpreter([">=3.12 <4", sys.executable], cache=session_cache) +@pytest.mark.parametrize("use_iterator", [False, True]) +def test_discovery_fallback_reuses_preferred_paths( + tmp_path: Path, session_cache: DiskCache, *, use_iterator: bool +) -> None: + preferred = tmp_path / "preferred" + subprocess.check_call([sys.executable, "-m", "venv", "--without-pip", "--copies", str(preferred)]) + executable = preferred / ("Scripts/python.exe" if IS_WIN else "bin/python") + paths = [str(executable)] + result = get_interpreter( + ["missing-interpreter", str(sys.version_info.major)], + try_first_with=iter(paths) if use_iterator else paths, + cache=session_cache, + env={**os.environ, "PATH": str(tmp_path), "UV_PYTHON_INSTALL_DIR": str(tmp_path / "missing")}, + predicate=lambda info: Path(info.executable) == executable, + ) + assert result is not None + assert Path(result.executable) == executable + + @pytest.fixture def mock_find_interpreter(mocker: MockerFixture) -> None: mocker.patch( From b3fb1e8bad3e5a8e786a4261c1dfbbb761c1b1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 07:46:04 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20fix(discovery):=20keep=20pre?= =?UTF-8?q?ferred=20paths=20lazy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python_discovery/_discovery.py | 6 +++--- tests/test_discovery.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/python_discovery/_discovery.py b/src/python_discovery/_discovery.py index 1263bd9..99fbb59 100644 --- a/src/python_discovery/_discovery.py +++ b/src/python_discovery/_discovery.py @@ -5,6 +5,7 @@ import sys from contextlib import suppress from dataclasses import dataclass +from itertools import tee from pathlib import Path from typing import TYPE_CHECKING, Final @@ -47,9 +48,8 @@ def get_interpreter( :return: the first matching interpreter, or ``None`` if no match is found. """ specs = [key] if isinstance(key, str) else key - first_with = tuple(try_first_with or ()) - for spec_str in specs: - if result := _find_interpreter(spec_str, first_with, cache, env, predicate): + for spec_str, preferred_paths in zip(specs, tee(try_first_with or (), len(specs))): + if result := _find_interpreter(spec_str, preferred_paths, cache, env, predicate): return result return None diff --git a/tests/test_discovery.py b/tests/test_discovery.py index 2bca3b1..c418114 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -133,7 +133,10 @@ def test_discovery_invalid_specifier_raises(session_cache: DiskCache) -> None: get_interpreter([">=3.12 <4", sys.executable], cache=session_cache) -@pytest.mark.parametrize("use_iterator", [False, True]) +@pytest.mark.parametrize( + "use_iterator", + [pytest.param(False, id="list"), pytest.param(True, id="iterator")], +) def test_discovery_fallback_reuses_preferred_paths( tmp_path: Path, session_cache: DiskCache, *, use_iterator: bool ) -> None: @@ -152,6 +155,13 @@ def test_discovery_fallback_reuses_preferred_paths( assert Path(result.executable) == executable +def test_discovery_stops_reading_preferred_paths_after_match(session_cache: DiskCache) -> None: + preferred_paths = iter([sys.executable, "unneeded"]) + result = get_interpreter(str(sys.version_info.major), try_first_with=preferred_paths, cache=session_cache) + assert result is not None + assert next(preferred_paths) == "unneeded" + + @pytest.fixture def mock_find_interpreter(mocker: MockerFixture) -> None: mocker.patch(