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]

Reply via email to