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 authored and gaborbernat committed Nov 10, 2022
1 parent dbc57c2 commit 1f9d4be
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 1 deletion.
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`.
19 changes: 18 additions & 1 deletion src/virtualenv/discovery/py_info.py
Expand Up @@ -138,7 +138,24 @@ 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
return base_executable
if os.path.exists(base_executable):
return base_executable
# Python may return "python" because it was invoked from the POSIX virtual environment
# however some installs/distributions do not provide a version-less "python" binary in
# the system install location (see PEP 394) so try to fallback to a versioned binary.
#
# Gate this to Python 3.11 as `sys._base_executable` path resolution is now relative to
# the 'home' key from pyvenv.cfg which often points to the system install location.
major, minor = self.version_info.major, self.version_info.minor
if self.os == "posix" and (major, minor) >= (3, 11):
# search relative to the directory of sys._base_executable
base_dir = os.path.dirname(base_executable)
for base_executable in [
os.path.join(base_dir, exe)
for exe in ("python{}".format(major), "python{}.{}".format(major, minor))
]:
if os.path.exists(base_executable):
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
# note we must choose the original and not the pure executable as shim scripts might throw us off
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 1f9d4be

Please sign in to comment.