Skip to content

Commit

Permalink
Try alternate filenames for system_executable
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
vfazio committed Nov 10, 2022
1 parent 1e90b97 commit 32a7343
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/changelog/2442.bugfix.rst
@@ -0,0 +1 @@
In POSIX virtual environments, try alternate binary names if `sys._base_executable` does not exist - by :user:`vfazio`.
15 changes: 15 additions & 0 deletions src/virtualenv/discovery/py_info.py
Expand Up @@ -138,6 +138,21 @@ def _fast_get_system_executable(self):
base_executable = getattr(sys, "_base_executable", None) # some platforms may set this to help us
if base_executable is not None: # use the saved system executable if present
if sys.executable != base_executable: # we know we're in a virtual environment, cannot be us
# Do light validation for non-Windows virtual environments
# Some installs/distributions do not provide a version-less "python" binary (PEP 394).
# Try to fallback to an alternative based on version number
# This should still be relatively fast since it's just a couple of `stat` calls
if self.os == "posix" and (self.version_info.major, self.version_info.minor) >= (3, 11):
if not os.path.exists(base_executable):
major, minor = self.version_info.major, self.version_info.minor
base_dir = os.path.dirname(base_executable)
for candidate in [
os.path.join(base_dir, "{}".format(exe))
for exe in ("python{}".format(major), "python{}.{}".format(major, minor))
]:
if os.path.exists(candidate):
base_executable = candidate
break
return base_executable
return None # in this case we just can't tell easily without poking around FS and calling them, bail
# if we're not in a virtual environment, this is already a system python, so return the original executable
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/discovery/py_info/test_py_info.py
Expand Up @@ -378,6 +378,29 @@ def test_custom_venv_install_scheme_is_prefered(mocker):
assert pyinfo.install_path("purelib").replace(os.sep, "/") == f"lib/python{pyver}/site-packages"


@pytest.mark.skipif(not (os.name == "posix" and sys.version_info[:2] >= (3, 11)), reason="POSIX 3.11+ specific")
def test_fallback_existent_system_executable(mocker):
current = PythonInfo()
# Posix may execute a "python" out of a venv but try to set the base_executable
# to "python" out of the system installation path. PEP 394 informs distributions
# that "python" is not required and the standard `make install` does not provide one

# Falsify some data to look like we're in a venv
current.prefix = current.exec_prefix = "/tmp/tmp.izZNCyINRj/venv"
current.executable = current.original_executable = os.path.join(current.prefix, "bin/python")

# Since we don't know if the distribution we're on provides python, use a binary that should not exist
mocker.patch.object(sys, "_base_executable", os.path.join(os.path.dirname(current.system_executable), "idontexist"))
mocker.patch.object(sys, "executable", current.executable)

# ensure it falls back to an alternate binary name that exists
current._fast_get_system_executable()
assert os.path.basename(current.system_executable) in [
f"python{v}" for v in (current.version_info.major, f"{current.version_info.major}.{current.version_info.minor}")
]
assert os.path.exists(current.system_executable)


@pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="3.10 specific")
def test_uses_posix_prefix_on_debian_3_10_without_venv(mocker):
# this is taken from ubuntu 22.04 /usr/lib/python3.10/sysconfig.py
Expand Down

0 comments on commit 32a7343

Please sign in to comment.