https://github.com/python/cpython/commit/e07817d02974cd574c453b4f7e84be57331abd05
commit: e07817d02974cd574c453b4f7e84be57331abd05
branch: main
author: Jonathan J. Helmus <[email protected]>
committer: ambv <[email protected]>
date: 2026-10-20T17:09:54-07:00
summary:
gh-106045: Fix venv path discovery for symlinked executables (#156483)
Use the resolved base executable when locating the base installation
instead of relying on the home directory in pyvenv.cfg.
Add tests covering relocated Python installations and misleading
standard-library landmarks near executable symlinks.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-08-27-17-26-21.gh-issue-106045.wzTH7f.rst
M Lib/test/test_getpath.py
M Modules/getpath.py
diff --git a/Lib/test/test_getpath.py b/Lib/test/test_getpath.py
index 83f09f3495547a..2907abe4b4cee6 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 00000000000000..a092b72d2ecdaa
--- /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 c322d5c4b4c251..f5f928f1795de5 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
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]