diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py index 83f09f3495547ac..2907abe4b4cee67 100644 --- a/Lib/test/test_getpath.py +++ b/Lib/test/test_getpath.py @@ -407,6 +407,74 @@ def test_venv_changed_name_posix(self): actual = getpath(ns, expected) self.assertEqual(expected, actual) + def test_venv_relocated_base_installation_posix(self): + "Test a venv whose base installation was relocated from PREFIX." + ns = MockPosixNamespace( + argv0="/venv/bin/python", + PREFIX="/build/install", + ) + ns.add_known_xfile("/relocated/bin/python3") + ns.add_known_xfile("/public/bin/python") + ns.add_known_link("/public/bin/python", "/relocated/bin/python3") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_link("/venv/bin/python", "/public/bin/python") + ns.add_known_file("/relocated/lib/python9.8/os.py") + ns.add_known_dir("/relocated/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + "home = /public/bin", + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/venv", + exec_prefix="/venv", + base_executable="/relocated/bin/python3", + base_prefix="/relocated", + base_exec_prefix="/relocated", + module_search_paths_set=1, + module_search_paths=[ + "/relocated/lib/python98.zip", + "/relocated/lib/python9.8", + "/relocated/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + + def test_venv_symlink_with_misleading_landmarks_posix(self): + "Test that landmarks near an external symlink do effect resolution." + ns = MockPosixNamespace( + argv0="/venv/bin/python", + PREFIX="/opt/python", + ) + ns.add_known_xfile("/opt/python/bin/python3") + ns.add_known_xfile("/public/bin/python") + ns.add_known_link("/public/bin/python", "/opt/python/bin/python3") + ns.add_known_xfile("/venv/bin/python") + ns.add_known_link("/venv/bin/python", "/public/bin/python") + ns.add_known_file("/opt/python/lib/python9.8/os.py") + ns.add_known_dir("/opt/python/lib/python9.8/lib-dynload") + ns.add_known_file("/public/lib/python9.8/os.py") + ns.add_known_dir("/public/lib/python9.8/lib-dynload") + ns.add_known_file("/venv/pyvenv.cfg", [ + "home = /public/bin", + ]) + expected = dict( + executable="/venv/bin/python", + prefix="/venv", + exec_prefix="/venv", + base_executable="/opt/python/bin/python3", + base_prefix="/opt/python", + base_exec_prefix="/opt/python", + module_search_paths_set=1, + module_search_paths=[ + "/opt/python/lib/python98.zip", + "/opt/python/lib/python9.8", + "/opt/python/lib/python9.8/lib-dynload", + ], + ) + actual = getpath(ns, expected) + self.assertEqual(expected, actual) + def test_venv_non_installed_zip_path_posix(self): "Test a venv created from non-installed python has correct zip path.""" ns = MockPosixNamespace( diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-17-26-21.gh-issue-106045.wzTH7f.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-17-26-21.gh-issue-106045.wzTH7f.rst new file mode 100644 index 000000000000000..a092b72d2ecdaa9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-17-26-21.gh-issue-106045.wzTH7f.rst @@ -0,0 +1 @@ +Fix standard library discovery in virtual environments created from a Python executable symlink located outside its base installation. diff --git a/Modules/getpath.py b/Modules/getpath.py index c322d5c4b4c251d..f5f928f1795de53 100644 --- a/Modules/getpath.py +++ b/Modules/getpath.py @@ -409,6 +409,10 @@ def search_up(prefix, *landmarks, test=isfile): if isfile(candidate): base_executable = candidate break + if base_executable and isfile(base_executable): + # Search relative to the resolved base executable rather + # than the potentially external symlink in pyvenv.cfg. + executable_dir = real_executable_dir = dirname(base_executable) # home key found; stop iterating over lines break