Skip to content

Commit 26b18ca

Browse files
committed
Make virtualenv use sysconfig to find paths
Instead of having our own copies of what we believe paths should be, or manually mutating what sysconfig returns for other variables, we should just delegate all of this to sysconfig in the standard way. This fixes #49545, fixing Python 3.12.8 support, where sysconfig stopped overly caching certain variables, thus leading to oddities after we've rewritten sys.exec_prefix and sys.prefix.
1 parent 2b68f03 commit 26b18ca

File tree

1 file changed

+49
-43
lines changed

1 file changed

+49
-43
lines changed

tools/wpt/virtualenv.py

+49-43
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import site
77
import sys
88
import sysconfig
9-
from pathlib import Path
109
from shutil import which
1110

1211
# The `pkg_resources` module is provided by `setuptools`, which is itself a
@@ -50,11 +49,40 @@ def create(self):
5049
self._working_set = None
5150
call(*self.virtualenv, self.path)
5251

52+
def get_paths(self):
53+
"""Wrapper around sysconfig.get_paths(), returning the appropriate paths for the env."""
54+
if "venv" in sysconfig.get_scheme_names():
55+
# This should always be used on Python 3.11 and above.
56+
scheme = "venv"
57+
elif os.name == "nt":
58+
# This matches nt_venv, unless sysconfig has been modified.
59+
scheme = "nt"
60+
elif os.name == "posix":
61+
# This matches posix_venv, unless sysconfig has been modified.
62+
scheme = "posix_prefix"
63+
elif sys.version_info >= (3, 10):
64+
# Using the default scheme is somewhat fragile, as various Python
65+
# distributors (e.g., what Debian and Fedora package, and what Xcode
66+
# includes) change the default scheme away from the upstream
67+
# defaults, but it's about as good as we can do.
68+
scheme = sysconfig.get_default_scheme()
69+
else:
70+
# This is explicitly documented as having previously existed in the 3.10
71+
# docs, and has existed since CPython 2.7 and 3.1 (but not 3.0).
72+
scheme = sysconfig._get_default_scheme()
73+
74+
vars = {
75+
"base": self.path,
76+
"platbase": self.path,
77+
"installed_base": self.path,
78+
"installed_platbase": self.path,
79+
}
80+
81+
return sysconfig.get_paths(scheme, vars)
82+
5383
@property
5484
def bin_path(self):
55-
if sys.platform in ("win32", "cygwin"):
56-
return os.path.join(self.path, "Scripts")
57-
return os.path.join(self.path, "bin")
85+
return self.get_paths()["scripts"]
5886

5987
@property
6088
def pip_path(self):
@@ -67,24 +95,10 @@ def pip_path(self):
6795

6896
@property
6997
def lib_path(self):
70-
base = self.path
71-
72-
# this block is literally taken from virtualenv 16.4.3
73-
IS_PYPY = hasattr(sys, "pypy_version_info")
74-
IS_JYTHON = sys.platform.startswith("java")
75-
if IS_JYTHON:
76-
site_packages = os.path.join(base, "Lib", "site-packages")
77-
elif IS_PYPY:
78-
site_packages = os.path.join(base, "site-packages")
79-
else:
80-
IS_WIN = sys.platform == "win32"
81-
if IS_WIN:
82-
site_packages = os.path.join(base, "Lib", "site-packages")
83-
else:
84-
version = f"{sys.version_info.major}.{sys.version_info.minor}"
85-
site_packages = os.path.join(base, "lib", f"python{version}", "site-packages")
86-
87-
return site_packages
98+
# We always return platlib here, even if it differs to purelib, because we can
99+
# always install pure-Python code into the platlib safely too. It's also very
100+
# unlikely to differ for a venv.
101+
return self.get_paths()["platlib"]
88102

89103
@property
90104
def working_set(self):
@@ -97,43 +111,35 @@ def working_set(self):
97111
return self._working_set
98112

99113
def activate(self):
100-
if sys.platform == 'darwin':
114+
if sys.platform == "darwin":
101115
# The default Python on macOS sets a __PYVENV_LAUNCHER__ environment
102116
# variable which affects invocation of python (e.g. via pip) in a
103117
# virtualenv. Unset it if present to avoid this. More background:
104118
# https://github.com/web-platform-tests/wpt/issues/27377
105119
# https://github.com/python/cpython/pull/9516
106-
os.environ.pop('__PYVENV_LAUNCHER__', None)
120+
os.environ.pop("__PYVENV_LAUNCHER__", None)
121+
122+
paths = self.get_paths()
107123

108124
# Setup the path and site packages as if we'd launched with the virtualenv active
109-
bin_dir = os.path.join(self.path, "bin")
125+
bin_dir = paths["scripts"]
110126
os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep))
127+
128+
# While not required (`./venv/bin/python3` won't set it, but
129+
# `source ./venv/bin/activate && python3` will), we have historically set this.
111130
os.environ["VIRTUAL_ENV"] = self.path
112131

113132
prev_length = len(sys.path)
114133

115-
schemes = sysconfig.get_scheme_names()
116-
if "venv" in schemes:
117-
scheme = "venv"
118-
else:
119-
scheme = "nt" if os.name == "nt" else "posix_user"
120-
sys_paths = sysconfig.get_paths(scheme)
121-
data_path = sys_paths["data"]
122-
added = set()
123134
# Add the venv library paths as sitedirs.
124-
# This converts system paths like /usr/local/lib/python3.10/site-packages
125-
# to venv-relative paths like {self.path}/lib/python3.10/site-packages and adds
126-
# those paths as site dirs to be used for module import.
127135
for key in ["purelib", "platlib"]:
128-
host_path = Path(sys_paths[key])
129-
relative_path = host_path.relative_to(data_path)
130-
site_dir = os.path.normpath(os.path.normcase(Path(self.path) / relative_path))
131-
if site_dir not in added:
132-
site.addsitedir(site_dir)
133-
added.add(site_dir)
136+
site.addsitedir(paths[key])
137+
138+
# Rearrange the path
134139
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
135140

136-
sys.real_prefix = sys.prefix
141+
# Change prefixes, similar to what initconfig/site does for venvs.
142+
sys.exec_prefix = self.path
137143
sys.prefix = self.path
138144

139145
def start(self):

0 commit comments

Comments
 (0)