From 111202286995334e0a9aa8104d6879f3b0ddd106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 16:59:15 -0800 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix(py-info):=20stop=20repea?= =?UTF-8?q?ted=20base=20probes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A candidate can report the same base directory with changing path spellings and evade the prefix cycle check. Compare canonical paths while keeping the reported path for interpreter discovery. --- docs/changelog/190.bugfix.rst | 1 + src/python_discovery/_py_info.py | 6 ++++-- tests/test_py_info_extra.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 docs/changelog/190.bugfix.rst diff --git a/docs/changelog/190.bugfix.rst b/docs/changelog/190.bugfix.rst new file mode 100644 index 0000000..1297457 --- /dev/null +++ b/docs/changelog/190.bugfix.rst @@ -0,0 +1 @@ +Stop repeated interpreter queries when a virtual environment reports the same base prefix with different path spellings. diff --git a/src/python_discovery/_py_info.py b/src/python_discovery/_py_info.py index 09733de..f4d26b1 100644 --- a/src/python_discovery/_py_info.py +++ b/src/python_discovery/_py_info.py @@ -10,6 +10,7 @@ import sys from collections import OrderedDict from itertools import product +from os.path import normcase, realpath from string import digits from typing import TYPE_CHECKING, ClassVar, Final, NamedTuple, TypedDict @@ -462,7 +463,8 @@ def resolve_to_system(cls, cache: PyInfoCache | None, target: PythonInfo) -> Pyt prefixes = OrderedDict() while target.system_executable is None: prefix = target.real_prefix or target.base_prefix or target.prefix - if prefix in prefixes: + prefix_key = normcase(realpath(prefix)) + if prefix_key in prefixes: if len(prefixes) == 1: _LOGGER.info("%r links back to itself via prefixes", target) target.system_executable = target.executable @@ -472,7 +474,7 @@ def resolve_to_system(cls, cache: PyInfoCache | None, target: PythonInfo) -> Pyt _LOGGER.error("%d: prefix=%s, info=%r", len(prefixes) + 1, prefix, target) msg = "prefixes are causing a circle {}".format("|".join(prefixes.keys())) raise RuntimeError(msg) - prefixes[prefix] = target + prefixes[prefix_key] = target target = target.discover_exe(cache, prefix=prefix, exact=False) if target.executable != target.system_executable: resolved = cls.from_exe(target.system_executable, cache) diff --git a/tests/test_py_info_extra.py b/tests/test_py_info_extra.py index 016636a..deedb5a 100644 --- a/tests/test_py_info_extra.py +++ b/tests/test_py_info_extra.py @@ -168,6 +168,21 @@ def test_resolve_to_system_single_prefix_self_link(mocker: MockerFixture, caplog assert any("links back to itself" in r.message for r in caplog.records) +def test_resolve_to_system_repeated_prefix_with_parent_segments(tmp_path: Path, mocker: MockerFixture) -> None: + (tmp_path / "child").mkdir() + target = PythonInfo() + target.system_executable = target.real_prefix = None + target.base_prefix = str(tmp_path) + repeated = PythonInfo() + repeated.system_executable = repeated.real_prefix = None + repeated.base_prefix = str(tmp_path / "child" / "..") + mocker.patch.object(PythonInfo, "discover_exe", return_value=repeated) + + result = PythonInfo.resolve_to_system(None, target) + + assert result.system_executable == repeated.executable + + def test_discover_exe_cache_hit() -> None: info = copy.deepcopy(CURRENT) cached = copy.deepcopy(CURRENT) From 68e57e08565e25d3ee470a6579687c961a93d685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Mon, 2 Nov 2026 17:02:12 -0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(news):=20link=20prefix?= =?UTF-8?q?=20fix=20to=20PR=20219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog/{190.bugfix.rst => 219.bugfix.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/changelog/{190.bugfix.rst => 219.bugfix.rst} (100%) diff --git a/docs/changelog/190.bugfix.rst b/docs/changelog/219.bugfix.rst similarity index 100% rename from docs/changelog/190.bugfix.rst rename to docs/changelog/219.bugfix.rst