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..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,8 +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 - for spec_str in specs: - if result := _find_interpreter(spec_str, try_first_with or (), 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 fd5338b..c418114 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -133,6 +133,35 @@ 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", + [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: + 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 + + +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(