Skip to content

Commit 1f9d4be

Browse files
vfaziogaborbernat
authored andcommitted
Try alternate filenames for system_executable
The value of `sys._base_executable` may not be a real file due to changes made in CPython 3.11. The value is derived from the current executable name and the "home" key from pyvenv.cfg. On POSIX systems, virtual environments deploy "python" for use within the venv however CPython's `make install` and a number of distributions do not provide a system "python" in part because of PEP 394. Virtualenv exposes this via `PythonInfo.system_executable` and can encounter issues when attempting to execute a non-existent file. Attempt to fallback to "python<MAJOR>" and "python<MAJOR>.<MINOR>" if "python" does not exist. Signed-off-by: Vincent Fazio <vfazio@gmail.com>
1 parent dbc57c2 commit 1f9d4be

File tree

3 files changed

+42
-1
lines changed

3 files changed

+42
-1
lines changed

docs/changelog/2442.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
In POSIX virtual environments, try alternate binary names if ``sys._base_executable`` does not exist - by :user:`vfazio`.

src/virtualenv/discovery/py_info.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,24 @@ def _fast_get_system_executable(self):
138138
base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
139139
if base_executable is not None: # use the saved system executable if present
140140
if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us
141-
return base_executable
141+
if os.path.exists(base_executable):
142+
return base_executable
143+
# Python may return "python" because it was invoked from the POSIX virtual environment
144+
# however some installs/distributions do not provide a version-less "python" binary in
145+
# the system install location (see PEP 394) so try to fallback to a versioned binary.
146+
#
147+
# Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to
148+
# the 'home' key from pyvenv.cfg which often points to the system install location.
149+
major, minor = self.version_info.major, self.version_info.minor
150+
if self.os == "posix" and (major, minor) >= (3, 11):
151+
# search relative to the directory of sys._base_executable
152+
base_dir = os.path.dirname(base_executable)
153+
for base_executable in [
154+
os.path.join(base_dir, exe)
155+
for exe in ("python{}".format(major), "python{}.{}".format(major, minor))
156+
]:
157+
if os.path.exists(base_executable):
158+
return base_executable
142159
return None # in this case we just can't tell easily without poking around FS and calling them, bail
143160
# if we're not in a virtual environment, this is already a system python, so return the original executable
144161
# note we must choose the original and not the pure executable as shim scripts might throw us off

tests/unit/discovery/py_info/test_py_info.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,29 @@ def test_custom_venv_install_scheme_is_prefered(mocker):
378378
assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages"
379379

380380

381+
@pytest.mark.skipif(not (os.name == "posix" and sys.version_info[:2] >= (3, 11)), reason="POSIX 3.11+ specific")
382+
def test_fallback_existent_system_executable(mocker):
383+
current = PythonInfo()
384+
# Posix may execute a "python" out of a venv but try to set the base_executable
385+
# to "python" out of the system installation path. PEP 394 informs distributions
386+
# that "python" is not required and the standard `make install` does not provide one
387+
388+
# Falsify some data to look like we're in a venv
389+
current.prefix = current.exec_prefix = "/tmp/tmp.izZNCyINRj/venv"
390+
current.executable = current.original_executable = os.path.join(current.prefix, "bin/python")
391+
392+
# Since we don't know if the distribution we're on provides python, use a binary that should not exist
393+
mocker.patch.object(sys, "_base_executable", os.path.join(os.path.dirname(current.system_executable), "idontexist"))
394+
mocker.patch.object(sys, "executable", current.executable)
395+
396+
# ensure it falls back to an alternate binary name that exists
397+
current._fast_get_system_executable()
398+
assert os.path.basename(current.system_executable) in [
399+
f"python{v}" for v in (current.version_info.major, f"{current.version_info.major}.{current.version_info.minor}")
400+
]
401+
assert os.path.exists(current.system_executable)
402+
403+
381404
@pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="3.10 specific")
382405
def test_uses_posix_prefix_on_debian_3_10_without_venv(mocker):
383406
# this is taken from ubuntu 22.04 /usr/lib/python3.10/sysconfig.py

0 commit comments

Comments
 (0)