Skip to content

Commit 32a7343

Browse files
committed
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 1e90b97 commit 32a7343

File tree

3 files changed

+39
-0
lines changed

3 files changed

+39
-0
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: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,21 @@ 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+
# Do light validation for non-Windows virtual environments
142+
# Some installs/distributions do not provide a version-less "python" binary (PEP 394).
143+
# Try to fallback to an alternative based on version number
144+
# This should still be relatively fast since it's just a couple of `stat` calls
145+
if self.os == "posix" and (self.version_info.major, self.version_info.minor) >= (3, 11):
146+
if not os.path.exists(base_executable):
147+
major, minor = self.version_info.major, self.version_info.minor
148+
base_dir = os.path.dirname(base_executable)
149+
for candidate in [
150+
os.path.join(base_dir, "{}".format(exe))
151+
for exe in ("python{}".format(major), "python{}.{}".format(major, minor))
152+
]:
153+
if os.path.exists(candidate):
154+
base_executable = candidate
155+
break
141156
return base_executable
142157
return None # in this case we just can't tell easily without poking around FS and calling them, bail
143158
# if we're not in a virtual environment, this is already a system python, so return the original executable

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)