Skip to content

Commit 9ee93da

Browse files
authored
Revert a large part of the wheel removal, to support Python 3.8 (#2876)
1 parent f900bb6 commit 9ee93da

File tree

10 files changed

+65
-46
lines changed

10 files changed

+65
-46
lines changed

docs/changelog/2868.feature.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
No longer bundle ``wheel`` wheels, ``setuptools`` includes native ``bdist_wheel`` support.
2-
Update ``pip`` to ``25.1``.
1+
No longer bundle ``wheel`` wheels (except on Python 3.8), ``setuptools`` includes native ``bdist_wheel`` support. Update ``pip`` to ``25.1``.

docs/user_guide.rst

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ The tool works in two phases:
4242
four further sub-steps:
4343

4444
- create a python that matches the target python interpreter from phase 1,
45-
- install (bootstrap) seed packages (one or more of :pypi:`pip`, :pypi:`setuptools`) in the created
45+
- install (bootstrap) seed packages (one or more of :pypi:`pip`, :pypi:`setuptools`, :pypi:`wheel`) in the created
4646
virtual environment,
4747
- install activation scripts into the binary directory of the virtual environment (these will allow end users to
4848
*activate* the virtual environment from various shells).
@@ -138,10 +138,9 @@ at the moment has two types of virtual environments:
138138

139139
Seeders
140140
-------
141-
These will install for you some seed packages (one or more of: :pypi:`pip`, :pypi:`setuptools`) that
141+
These will install for you some seed packages (one or more of: :pypi:`pip`, :pypi:`setuptools`, :pypi:`wheel`) that
142142
enables you to install additional python packages into the created virtual environment (by invoking pip). Installing
143-
:pypi:`setuptools` is disabled by default on Python 3.12+ environments. There are two
144-
main seed mechanisms available:
143+
:pypi:`setuptools` is disabled by default on Python 3.12+ environments. :pypi:`wheel` is only installed on Python 3.8, by default. There are two main seed mechanisms available:
145144

146145
- ``pip`` - this method uses the bundled pip with virtualenv to install the seed packages (note, a new child process
147146
needs to be created to do this, which can be expensive especially on Windows).
@@ -163,8 +162,8 @@ Wheels
163162
To install a seed package via either ``pip`` or ``app-data`` method virtualenv needs to acquire a wheel of the target
164163
package. These wheels may be acquired from multiple locations as follows:
165164

166-
- ``virtualenv`` ships out of box with a set of embed ``wheels`` for both seed packages (:pypi:`pip`,
167-
:pypi:`setuptools`). These are packaged together with the virtualenv source files, and only change upon
165+
- ``virtualenv`` ships out of box with a set of embed ``wheels`` for all three seed packages (:pypi:`pip`,
166+
:pypi:`setuptools`, :pypi:`wheel`). These are packaged together with the virtualenv source files, and only change upon
168167
upgrading virtualenv. Different Python versions require different versions of these, and because virtualenv supports a
169168
wide range of Python versions, the number of embedded wheels out of box is greater than 3. Whenever newer versions of
170169
these embedded packages are released upstream ``virtualenv`` project upgrades them, and does a new release. Therefore,

src/virtualenv/seed/embed/base_embed.py

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@ def __init__(self, options) -> None:
2020

2121
self.pip_version = options.pip
2222
self.setuptools_version = options.setuptools
23-
24-
self.no_pip = options.no_pip
25-
self.no_setuptools = options.no_setuptools
26-
self.app_data = options.app_data
27-
self.periodic_update = not options.no_periodic_update
28-
29-
if options.no_wheel:
23+
if hasattr(options, "wheel"):
24+
# Python 3.8
25+
self.wheel_version = options.wheel
26+
self.no_wheel = options.no_wheel
27+
elif options.no_wheel:
3028
warn(
3129
"The --no-wheel option is deprecated. "
32-
"It has no effect, wheel is no longer bundled in virtualenv. "
33-
"This option will be removed in pip 26.",
30+
"It has no effect for Python >= 3.8 as wheel is no longer "
31+
"bundled in virtualenv.",
3432
DeprecationWarning,
3533
stacklevel=1,
3634
)
3735

36+
self.no_pip = options.no_pip
37+
self.no_setuptools = options.no_setuptools
38+
self.app_data = options.app_data
39+
self.periodic_update = not options.no_periodic_update
40+
3841
if not self.distribution_to_versions():
3942
self.enabled = False
4043

@@ -43,13 +46,14 @@ def distributions(cls) -> dict[str, Version]:
4346
return {
4447
"pip": Version.bundle,
4548
"setuptools": Version.bundle,
49+
"wheel": Version.bundle,
4650
}
4751

4852
def distribution_to_versions(self) -> dict[str, str]:
4953
return {
5054
distribution: getattr(self, f"{distribution}_version")
5155
for distribution in self.distributions()
52-
if getattr(self, f"no_{distribution}") is False and getattr(self, f"{distribution}_version") != "none"
56+
if getattr(self, f"no_{distribution}", None) is False and getattr(self, f"{distribution}_version") != "none"
5357
}
5458

5559
@classmethod
@@ -81,6 +85,8 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
8185
for distribution, default in cls.distributions().items():
8286
if interpreter.version_info[:2] >= (3, 12) and distribution == "setuptools":
8387
default = "none" # noqa: PLW2901
88+
if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
89+
continue
8490
parser.add_argument(
8591
f"--{distribution}",
8692
dest=distribution,
@@ -89,20 +95,16 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
8995
default=default,
9096
)
9197
for distribution in cls.distributions():
98+
help_ = f"do not install {distribution}"
99+
if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
100+
help_ = SUPPRESS
92101
parser.add_argument(
93102
f"--no-{distribution}",
94103
dest=f"no_{distribution}",
95104
action="store_true",
96-
help=f"do not install {distribution}",
105+
help=help_,
97106
default=False,
98107
)
99-
# DEPRECATED: Remove in pip 26
100-
parser.add_argument(
101-
"--no-wheel",
102-
dest="no_wheel",
103-
action="store_true",
104-
help=SUPPRESS,
105-
)
106108
parser.add_argument(
107109
"--no-periodic-update",
108110
dest="no_periodic_update",
@@ -118,7 +120,7 @@ def __repr__(self) -> str:
118120
result += f"extra_search_dir={', '.join(str(i) for i in self.extra_search_dir)},"
119121
result += f"download={self.download},"
120122
for distribution in self.distributions():
121-
if getattr(self, f"no_{distribution}"):
123+
if getattr(self, f"no_{distribution}", None):
122124
continue
123125
version = getattr(self, f"{distribution}_version", None)
124126
if version == "none":

src/virtualenv/seed/wheels/embed/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"3.8": {
1010
"pip": "pip-25.0.1-py3-none-any.whl",
1111
"setuptools": "setuptools-75.3.2-py3-none-any.whl",
12+
"wheel": "wheel-0.45.1-py3-none-any.whl",
1213
},
1314
"3.9": {
1415
"pip": "pip-25.1-py3-none-any.whl",
Binary file not shown.

tests/unit/create/test_creator.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,10 +398,6 @@ def test_create_long_path(tmp_path):
398398
subprocess.check_call([str(result.creator.script("pip")), "--version"])
399399

400400

401-
@pytest.mark.skipif(
402-
sys.version_info[:2] == (3, 8),
403-
reason="Disable on Python 3.8, which still uses pip 25.0.1 (without https://github.com/pypa/pip/pull/13330)",
404-
)
405401
@pytest.mark.slow
406402
@pytest.mark.parametrize("creator", sorted(set(PythonInfo.current_system().creators().key_to_class) - {"builtin"}))
407403
@pytest.mark.usefixtures("session_app_data")
@@ -472,7 +468,7 @@ def list_files(path):
472468
def test_zip_importer_can_import_setuptools(tmp_path):
473469
"""We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8"""
474470
result = cli_run(
475-
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--copies", "--setuptools", "bundle"],
471+
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies", "--setuptools", "bundle"],
476472
)
477473
zip_path = tmp_path / "site-packages.zip"
478474
with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zip_handler:

tests/unit/seed/embed/test_base_embed.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,25 @@ def test_download_cli_flag(args, download, tmp_path):
2121
assert session.seeder.download is download
2222

2323

24-
# DEPRECATED: Remove in pip 26
24+
@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheels for Python 3.8")
2525
def test_download_deprecated_cli_flag(tmp_path):
2626
with warnings.catch_warnings(record=True) as w:
2727
warnings.simplefilter("always")
2828
session_via_cli(["--no-wheel", str(tmp_path)])
2929
assert len(w) == 1
3030
assert issubclass(w[-1].category, DeprecationWarning)
3131
assert str(w[-1].message) == (
32-
"The --no-wheel option is deprecated. "
33-
"It has no effect, wheel is no longer bundled in virtualenv. "
34-
"This option will be removed in pip 26."
32+
"The --no-wheel option is deprecated. It has no effect for Python >= "
33+
"3.8 as wheel is no longer bundled in virtualenv."
3534
)
3635

3736

3837
def test_embed_wheel_versions(tmp_path: Path) -> None:
3938
session = session_via_cli([str(tmp_path)])
40-
expected = {"pip": "bundle"} if sys.version_info[:2] >= (3, 12) else {"pip": "bundle", "setuptools": "bundle"}
39+
if sys.version_info[:2] >= (3, 12):
40+
expected = {"pip": "bundle"}
41+
elif sys.version_info[:2] >= (3, 9):
42+
expected = {"pip": "bundle", "setuptools": "bundle"}
43+
else:
44+
expected = {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"}
4145
assert session.seeder.distribution_to_versions() == expected

tests/unit/seed/embed/test_bootstrap_link_via_app_data.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
@pytest.mark.slow
2727
@pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True])
28-
def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies):
28+
def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies, for_py_version): # noqa: PLR0915
2929
current = PythonInfo.current_system()
3030
bundle_ver = BUNDLE_SUPPORT[current.version_release_str]
3131
create_cmd = [
@@ -45,6 +45,8 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
4545
current_fastest,
4646
"-vv",
4747
]
48+
if for_py_version == "3.8":
49+
create_cmd += ["--wheel", bundle_ver["wheel"].split("-")[1]]
4850
if not copies:
4951
create_cmd.append("--symlink-app-data")
5052
result = cli_run(create_cmd)
@@ -110,7 +112,7 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
110112
# Windows does not allow removing a executable while running it, so when uninstalling pip we need to do it via
111113
# python -m pip
112114
remove_cmd = [str(result.creator.exe), "-m", "pip", *remove_cmd[1:]]
113-
process = Popen([*remove_cmd, "pip"])
115+
process = Popen([*remove_cmd, "pip", "wheel"])
114116
_, __ = process.communicate()
115117
assert not process.returncode
116118
# pip is greedy here, removing all packages removes the site-package too
@@ -208,13 +210,20 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest
208210

209211

210212
@pytest.mark.slow
211-
@pytest.mark.parametrize("pkg", ["pip", "setuptools"])
213+
@pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"])
212214
@pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env")
213-
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg):
215+
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg, for_py_version):
216+
if for_py_version != "3.8" and pkg == "wheel":
217+
msg = "wheel isn't installed on Python > 3.8"
218+
raise pytest.skip(msg)
214219
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--setuptools", "bundle"]
220+
if for_py_version == "3.8":
221+
create_cmd += ["--wheel", "bundle"]
215222
result = cli_run(create_cmd)
216223
assert not (result.creator.purelib / pkg).exists()
217-
for key in {"pip", "setuptools"} - {pkg}:
224+
for key in {"pip", "setuptools", "wheel"} - {pkg}:
225+
if for_py_version != "3.8" and key == "wheel":
226+
continue
218227
assert (result.creator.purelib / key).exists()
219228

220229

@@ -239,7 +248,10 @@ def _run_parallel_threads(tmp_path):
239248

240249
def _run(name):
241250
try:
242-
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"])
251+
cmd = ["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"]
252+
if sys.version_info[:2] == (3, 8):
253+
cmd.append("--no-wheel")
254+
cli_run(cmd)
243255
except Exception as exception: # noqa: BLE001
244256
as_str = str(exception)
245257
exceptions.append(as_str)

tests/unit/seed/embed/test_pip_invoke.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414

1515
@pytest.mark.slow
16-
@pytest.mark.parametrize("no", ["pip", "setuptools", ""])
16+
@pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""])
1717
def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no): # noqa: C901
1818
extra_search_dir = tmp_path / "extra"
1919
extra_search_dir.mkdir()
@@ -50,6 +50,8 @@ def _execute(cmd, env):
5050
original = PipInvoke._execute # noqa: SLF001
5151
run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute)
5252
versions = {"pip": "embed", "setuptools": "bundle"}
53+
if sys.version_info[:2] == (3, 8):
54+
versions["wheel"] = new["wheel"].split("-")[1]
5355

5456
create_cmd = [
5557
"--seeder",
@@ -76,13 +78,16 @@ def _execute(cmd, env):
7678
site_package = result.creator.purelib
7779
pip = site_package / "pip"
7880
setuptools = site_package / "setuptools"
81+
wheel = site_package / "wheel"
7982
files_post_first_create = list(site_package.iterdir())
8083

8184
if no:
8285
no_file = locals()[no]
8386
assert no not in files_post_first_create
8487

85-
for key in ("pip", "setuptools"):
88+
for key in ("pip", "setuptools", "wheel"):
8689
if key == no:
8790
continue
91+
if sys.version_info[:2] >= (3, 9) and key == "wheel":
92+
continue
8893
assert locals()[key] in files_post_first_create

tests/unit/seed/wheels/test_periodic_update.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def _do_update( # noqa: PLR0913
7474
packages[args[1]["distribution"]].append(args[1]["for_py_version"])
7575
packages = {key: sorted(value) for key, value in packages.items()}
7676
versions = sorted(BUNDLE_SUPPORT.keys())
77-
expected = {"setuptools": versions, "pip": versions}
77+
expected = {"setuptools": versions, "wheel": ["3.8"], "pip": versions}
7878
assert packages == expected
7979

8080

@@ -97,6 +97,7 @@ def test_pick_periodic_update(tmp_path, mocker, for_py_version):
9797
"--activators",
9898
"",
9999
"--no-periodic-update",
100+
"--no-wheel",
100101
"--no-pip",
101102
"--setuptools",
102103
"bundle",

0 commit comments

Comments
 (0)