Skip to content

Stop including 'wheel', setuptools 70.1 has native bdist_wheel support #2868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/2868.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
No longer bundle ``wheel`` wheels, ``setuptools`` includes native ``bdist_wheel`` support.
Update ``pip`` to ``25.1``.
2 changes: 1 addition & 1 deletion docs/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ that folder.
Release
~~~~~~~

virtualenv's release schedule is tied to ``pip``, ``setuptools`` and ``wheel``. We bundle the latest version of these
virtualenv's release schedule is tied to ``pip`` and ``setuptools``. We bundle the latest version of these
libraries so each time there's a new version of any of these, there will be a new virtualenv release shortly afterwards
(we usually wait just a few days to avoid pulling in any broken releases).

Expand Down
10 changes: 5 additions & 5 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The tool works in two phases:
four further sub-steps:

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

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

- ``pip`` - this method uses the bundled pip with virtualenv to install the seed packages (note, a new child process
Expand All @@ -163,8 +163,8 @@ Wheels
To install a seed package via either ``pip`` or ``app-data`` method virtualenv needs to acquire a wheel of the target
package. These wheels may be acquired from multiple locations as follows:

- ``virtualenv`` ships out of box with a set of embed ``wheels`` for all three seed packages (:pypi:`pip`,
:pypi:`setuptools`, :pypi:`wheel`). These are packaged together with the virtualenv source files, and only change upon
- ``virtualenv`` ships out of box with a set of embed ``wheels`` for both seed packages (:pypi:`pip`,
:pypi:`setuptools`). These are packaged together with the virtualenv source files, and only change upon
upgrading virtualenv. Different Python versions require different versions of these, and because virtualenv supports a
wide range of Python versions, the number of embedded wheels out of box is greater than 3. Whenever newer versions of
these embedded packages are released upstream ``virtualenv`` project upgrades them, and does a new release. Therefore,
Expand Down
23 changes: 19 additions & 4 deletions src/virtualenv/seed/embed/base_embed.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

from abc import ABC
from argparse import SUPPRESS
from pathlib import Path
from warnings import warn

from virtualenv.seed.seeder import Seeder
from virtualenv.seed.wheels import Version
Expand All @@ -18,14 +20,21 @@ def __init__(self, options) -> None:

self.pip_version = options.pip
self.setuptools_version = options.setuptools
self.wheel_version = options.wheel

self.no_pip = options.no_pip
self.no_setuptools = options.no_setuptools
self.no_wheel = options.no_wheel
self.app_data = options.app_data
self.periodic_update = not options.no_periodic_update

if options.no_wheel:
warn(
"The --no-wheel option is deprecated. "
"It has no effect, wheel is no longer bundled in virtualenv. "
"This option will be removed in pip 26.",
DeprecationWarning,
stacklevel=1,
)

if not self.distribution_to_versions():
self.enabled = False

Expand All @@ -34,7 +43,6 @@ def distributions(cls) -> dict[str, Version]:
return {
"pip": Version.bundle,
"setuptools": Version.bundle,
"wheel": Version.bundle,
}

def distribution_to_versions(self) -> dict[str, str]:
Expand Down Expand Up @@ -71,7 +79,7 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
default=[],
)
for distribution, default in cls.distributions().items():
if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}:
if interpreter.version_info[:2] >= (3, 12) and distribution == "setuptools":
default = "none" # noqa: PLW2901
parser.add_argument(
f"--{distribution}",
Expand All @@ -88,6 +96,13 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
help=f"do not install {distribution}",
default=False,
)
# DEPRECATED: Remove in pip 26
parser.add_argument(
"--no-wheel",
dest="no_wheel",
action="store_true",
help=SUPPRESS,
)
parser.add_argument(
"--no-periodic-update",
dest="no_periodic_update",
Expand Down
19 changes: 6 additions & 13 deletions src/virtualenv/seed/wheels/embed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,30 @@
"3.8": {
"pip": "pip-25.0.1-py3-none-any.whl",
"setuptools": "setuptools-75.3.2-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.9": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.10": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.11": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.12": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.13": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
"3.14": {
"pip": "pip-25.0.1-py3-none-any.whl",
"pip": "pip-25.1-py3-none-any.whl",
"setuptools": "setuptools-78.1.0-py3-none-any.whl",
"wheel": "wheel-0.45.1-py3-none-any.whl",
},
}
MAX = "3.8"
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions tests/unit/config/test___main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,15 @@ def test_fail_with_traceback(raise_on_session_done, tmp_path, capsys):

@pytest.mark.usefixtures("session_app_data")
def test_session_report_full(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
run_with_catch([str(tmp_path), "--setuptools", "bundle", "--wheel", "bundle"])
run_with_catch([str(tmp_path), "--setuptools", "bundle"])
out, err = capsys.readouterr()
assert not err
lines = out.splitlines()
regexes = [
r"created virtual environment .* in \d+ms",
r" creator .*",
r" seeder .*",
r" added seed packages: .*pip==.*, setuptools==.*, wheel==.*",
r" added seed packages: .*pip==.*, setuptools==.*",
r" activators .*",
]
_match_regexes(lines, regexes)
Expand Down
8 changes: 5 additions & 3 deletions tests/unit/create/test_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ def test_create_long_path(tmp_path):
subprocess.check_call([str(result.creator.script("pip")), "--version"])


@pytest.mark.skipif(
sys.version_info[:2] == (3, 8),
reason="Disable on Python 3.8, which still uses pip 25.0.1 (without https://github.com/pypa/pip/pull/13330)",
)
@pytest.mark.slow
@pytest.mark.parametrize("creator", sorted(set(PythonInfo.current_system().creators().key_to_class) - {"builtin"}))
@pytest.mark.usefixtures("session_app_data")
Expand All @@ -411,8 +415,6 @@ def test_create_distutils_cfg(creator, tmp_path, monkeypatch):
creator,
"--setuptools",
"bundle",
"--wheel",
"bundle",
],
)

Expand Down Expand Up @@ -470,7 +472,7 @@ def list_files(path):
def test_zip_importer_can_import_setuptools(tmp_path):
"""We're patching the loaders so might fail on r/o loaders, such as zipimporter on CPython<3.8"""
result = cli_run(
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--copies", "--setuptools", "bundle"],
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--copies", "--setuptools", "bundle"],
)
zip_path = tmp_path / "site-packages.zip"
with zipfile.ZipFile(str(zip_path), "w", zipfile.ZIP_DEFLATED) as zip_handler:
Expand Down
21 changes: 16 additions & 5 deletions tests/unit/seed/embed/test_base_embed.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
import warnings
from typing import TYPE_CHECKING

import pytest
Expand All @@ -20,11 +21,21 @@ def test_download_cli_flag(args, download, tmp_path):
assert session.seeder.download is download


# DEPRECATED: Remove in pip 26
def test_download_deprecated_cli_flag(tmp_path):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
session_via_cli(["--no-wheel", str(tmp_path)])
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert str(w[-1].message) == (
"The --no-wheel option is deprecated. "
"It has no effect, wheel is no longer bundled in virtualenv. "
"This option will be removed in pip 26."
)


def test_embed_wheel_versions(tmp_path: Path) -> None:
session = session_via_cli([str(tmp_path)])
expected = (
{"pip": "bundle"}
if sys.version_info[:2] >= (3, 12)
else {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"}
)
expected = {"pip": "bundle"} if sys.version_info[:2] >= (3, 12) else {"pip": "bundle", "setuptools": "bundle"}
assert session.seeder.distribution_to_versions() == expected
12 changes: 6 additions & 6 deletions tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
# Windows does not allow removing a executable while running it, so when uninstalling pip we need to do it via
# python -m pip
remove_cmd = [str(result.creator.exe), "-m", "pip", *remove_cmd[1:]]
process = Popen([*remove_cmd, "pip", "wheel"])
process = Popen([*remove_cmd, "pip"])
_, __ = process.communicate()
assert not process.returncode
# pip is greedy here, removing all packages removes the site-package too
Expand Down Expand Up @@ -208,13 +208,13 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest


@pytest.mark.slow
@pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"])
@pytest.mark.parametrize("pkg", ["pip", "setuptools"])
@pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env")
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg):
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--wheel", "bundle", "--setuptools", "bundle"]
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--setuptools", "bundle"]
result = cli_run(create_cmd)
assert not (result.creator.purelib / pkg).exists()
for key in {"pip", "setuptools", "wheel"} - {pkg}:
for key in {"pip", "setuptools"} - {pkg}:
assert (result.creator.purelib / key).exists()


Expand All @@ -230,7 +230,7 @@ def test_app_data_parallel_fail(tmp_path: Path, mocker: MockerFixture) -> None:
exceptions = _run_parallel_threads(tmp_path)
assert len(exceptions) == 2
for exception in exceptions:
assert exception.startswith("failed to build image wheel because:\nTraceback")
assert exception.startswith("failed to build image pip because:\nTraceback")
assert "RuntimeError" in exception, exception


Expand All @@ -239,7 +239,7 @@ def _run_parallel_threads(tmp_path):

def _run(name):
try:
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-pip", "--no-setuptools", "--wheel", "bundle"])
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"])
except Exception as exception: # noqa: BLE001
as_str = str(exception)
exceptions.append(as_str)
Expand Down
7 changes: 3 additions & 4 deletions tests/unit/seed/embed/test_pip_invoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


@pytest.mark.slow
@pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""])
@pytest.mark.parametrize("no", ["pip", "setuptools", ""])
def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_fastest, no): # noqa: C901
extra_search_dir = tmp_path / "extra"
extra_search_dir.mkdir()
Expand Down Expand Up @@ -49,7 +49,7 @@ def _execute(cmd, env):

original = PipInvoke._execute # noqa: SLF001
run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute)
versions = {"pip": "embed", "setuptools": "bundle", "wheel": new["wheel"].split("-")[1]}
versions = {"pip": "embed", "setuptools": "bundle"}

create_cmd = [
"--seeder",
Expand All @@ -76,14 +76,13 @@ def _execute(cmd, env):
site_package = result.creator.purelib
pip = site_package / "pip"
setuptools = site_package / "setuptools"
wheel = site_package / "wheel"
files_post_first_create = list(site_package.iterdir())

if no:
no_file = locals()[no]
assert no not in files_post_first_create

for key in ("pip", "setuptools", "wheel"):
for key in ("pip", "setuptools"):
if key == no:
continue
assert locals()[key] in files_post_first_create
5 changes: 1 addition & 4 deletions tests/unit/seed/wheels/test_periodic_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def _do_update( # noqa: PLR0913
packages[args[1]["distribution"]].append(args[1]["for_py_version"])
packages = {key: sorted(value) for key, value in packages.items()}
versions = sorted(BUNDLE_SUPPORT.keys())
expected = {"setuptools": versions, "wheel": versions, "pip": versions}
expected = {"setuptools": versions, "pip": versions}
assert packages == expected


Expand All @@ -97,12 +97,9 @@ def test_pick_periodic_update(tmp_path, mocker, for_py_version):
"--activators",
"",
"--no-periodic-update",
"--no-wheel",
"--no-pip",
"--setuptools",
"bundle",
"--wheel",
"bundle",
],
)

Expand Down
Loading