Skip to content

Commit fa93ca7

Browse files
committed
Favor the "venv" sysconfig install scheme over the default and distutils scheme
Python is preparing to allow re-distributors to set custom sysconfig install schemes in 3.11+: https://bugs.python.org/issue43976 Fedora is already adapting the default installation scheme to their needs: https://lists.fedoraproject.org/archives/list/[email protected]/thread/AAGUFQZ4RZDU7KUN4HA43KQJCMSFR3GW/ With either of the above, the distributors need to signalize the paths used in virtual environments somehow. When they set the "venv" install scheme in sysconfig, it is now favored over the default sysconfig scheme as well as over distutils. A similar technique was proposed to Python, for the venv module: https://bugs.python.org/issue45413 Fixes pypa#2208
1 parent 653d4eb commit fa93ca7

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

docs/changelog/2208.feature.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
If a ``"venv"`` install scheme exists in ``sysconfig``, virtualenv now uses it to create new virtual environments.
2+
This allows Python distributors, such as Fedora, to patch/replace the default install scheme without affecting
3+
the paths in new virtual environments.
4+
A similar technique `was proposed to Python, for the venv module <https://bugs.python.org/issue45413>`_ - by ``hroncok``

src/virtualenv/discovery/py_info.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,18 @@ def abs_path(v):
7373
self.file_system_encoding = u(sys.getfilesystemencoding())
7474
self.stdout_encoding = u(getattr(sys.stdout, "encoding", None))
7575

76-
self.sysconfig_paths = {u(i): u(sysconfig.get_path(i, expand=False)) for i in sysconfig.get_path_names()}
76+
if "venv" in sysconfig.get_scheme_names():
77+
self.sysconfig_scheme = "venv"
78+
self.sysconfig_paths = {
79+
u(i): u(sysconfig.get_path(i, expand=False, scheme="venv")) for i in sysconfig.get_path_names()
80+
}
81+
# we cannot use distutils at all if "venv" exists, distutils don't know it
82+
self.distutils_install = {}
83+
else:
84+
self.sysconfig_scheme = None
85+
self.sysconfig_paths = {u(i): u(sysconfig.get_path(i, expand=False)) for i in sysconfig.get_path_names()}
86+
self.distutils_install = {u(k): u(v) for k, v in self._distutils_install().items()}
87+
7788
# https://bugs.python.org/issue22199
7889
makefile = getattr(sysconfig, "get_makefile_filename", getattr(sysconfig, "_get_makefile_filename", None))
7990
self.sysconfig = {
@@ -95,7 +106,6 @@ def abs_path(v):
95106
if self.implementation == "PyPy" and sys.version_info.major == 2:
96107
self.sysconfig_vars[u"implementation_lower"] = u"python"
97108

98-
self.distutils_install = {u(k): u(v) for k, v in self._distutils_install().items()}
99109
confs = {k: (self.system_prefix if v.startswith(self.prefix) else v) for k, v in self.sysconfig_vars.items()}
100110
self.system_stdlib = self.sysconfig_path("stdlib", confs)
101111
self.system_stdlib_platform = self.sysconfig_path("platstdlib", confs)
@@ -119,7 +129,7 @@ def _fast_get_system_executable(self):
119129

120130
def install_path(self, key):
121131
result = self.distutils_install.get(key)
122-
if result is None: # use sysconfig if distutils is unavailable
132+
if result is None: # use sysconfig if sysconfig_scheme is set or distutils is unavailable
123133
# set prefixes to empty => result is relative from cwd
124134
prefixes = self.prefix, self.exec_prefix, self.base_prefix, self.base_exec_prefix
125135
config_var = {k: "" if v in prefixes else v for k, v in self.sysconfig_vars.items()}

tests/unit/discovery/py_info/test_py_info.py

+49
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import logging
77
import os
88
import sys
9+
import sysconfig
910
from collections import namedtuple
1011
from textwrap import dedent
1112

@@ -311,3 +312,51 @@ def test_py_info_to_system_raises(session_app_data, mocker, caplog, skip_if_test
311312
assert log.levelno == logging.INFO
312313
expected = "ignore {} due cannot resolve system due to RuntimeError('failed to detect ".format(sys.executable)
313314
assert expected in log.message
315+
316+
317+
def test_custom_venv_install_scheme_is_prefered(mocker):
318+
# The paths in this test are Fedora paths, but we set them for nt as well,
319+
# so the test also works on Windows, despite the actual values are nonsense there.
320+
# Values were simplified to be compatible with all the supported Python versions.
321+
# Note: Since this file has from __future__ import unicode_literals,
322+
# we manually cast all the values to str()
323+
# as the original schemes are not unicode on Python 2.
324+
default_scheme = {
325+
str("stdlib"): str("{base}/lib/python{py_version_short}"),
326+
str("platstdlib"): str("{platbase}/lib/python{py_version_short}"),
327+
str("purelib"): str("{base}/local/lib/python{py_version_short}/site-packages"),
328+
str("platlib"): str("{platbase}/local/lib/python{py_version_short}/site-packages"),
329+
str("include"): str("{base}/include/python{py_version_short}"),
330+
str("platinclude"): str("{platbase}/include/python{py_version_short}"),
331+
str("scripts"): str("{base}/local/bin"),
332+
str("data"): str("{base}/local"),
333+
}
334+
venv_scheme = {key: path.replace(str("local"), str()) for key, path in default_scheme.items()}
335+
sysconfig_install_schemes = {
336+
str("posix_prefix"): default_scheme,
337+
str("nt"): default_scheme,
338+
str("venv"): venv_scheme,
339+
}
340+
if getattr(sysconfig, "get_preferred_scheme", None):
341+
sysconfig_install_schemes[sysconfig.get_preferred_scheme("prefix")] = default_scheme
342+
mocker.patch("sysconfig._INSTALL_SCHEMES", sysconfig_install_schemes)
343+
344+
# On Python < 3.10, the distutils schemes are not derived from sysconfig schemes
345+
# So we mock them as well to assert the custom "venv" install scheme has priority
346+
distutils_scheme = {
347+
str("purelib"): str("$base/local/lib/python$py_version_short/site-packages"),
348+
str("platlib"): str("$platbase/local/lib/python$py_version_short/site-packages"),
349+
str("headers"): str("$base/include/python$py_version_short/$dist_name"),
350+
str("scripts"): str("$base/local/bin"),
351+
str("data"): str("$base/local"),
352+
}
353+
distutils_schemes = {
354+
str("unix_prefix"): distutils_scheme,
355+
str("nt"): distutils_scheme,
356+
}
357+
mocker.patch("distutils.command.install.INSTALL_SCHEMES", distutils_schemes)
358+
359+
pyinfo = PythonInfo()
360+
pyver = "{}.{}".format(pyinfo.version_info.major, pyinfo.version_info.minor)
361+
assert pyinfo.install_path("scripts") == "bin"
362+
assert pyinfo.install_path("purelib").replace(os.sep, "/") == "lib/python{}/site-packages".format(pyver)

0 commit comments

Comments
 (0)