Skip to content

Revert a large part of the wheel removal, to support Python 3.8 #2876

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
May 5, 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
3 changes: 1 addition & 2 deletions docs/changelog/2868.feature.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
No longer bundle ``wheel`` wheels, ``setuptools`` includes native ``bdist_wheel`` support.
Update ``pip`` to ``25.1``.
No longer bundle ``wheel`` wheels (except on Python 3.8), ``setuptools`` includes native ``bdist_wheel`` support. Update ``pip`` to ``25.1``.
11 changes: 5 additions & 6 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`) in the created
- install (bootstrap) seed packages (one or more of :pypi:`pip`, :pypi:`setuptools`, :pypi:`wheel`) 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,10 +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`) that
These will install for you some seed packages (one or more of: :pypi:`pip`, :pypi:`setuptools`, :pypi:`wheel`) that
enables you to install additional python packages into the created virtual environment (by invoking pip). Installing
:pypi:`setuptools` is disabled by default on Python 3.12+ environments. There are two
main seed mechanisms available:
: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:

- ``pip`` - this method uses the bundled pip with virtualenv to install the seed packages (note, a new child process
needs to be created to do this, which can be expensive especially on Windows).
Expand All @@ -163,8 +162,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 both seed packages (:pypi:`pip`,
:pypi:`setuptools`). 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 all three seed packages (:pypi:`pip`,
:pypi:`setuptools`, :pypi:`wheel`). 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
40 changes: 21 additions & 19 deletions src/virtualenv/seed/embed/base_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,24 @@ def __init__(self, options) -> None:

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

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

if options.no_wheel:
if hasattr(options, "wheel"):
# Python 3.8
self.wheel_version = options.wheel
self.no_wheel = options.no_wheel
elif 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.",
"It has no effect for Python >= 3.8 as wheel is no longer "
"bundled in virtualenv.",
DeprecationWarning,
stacklevel=1,
)

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

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

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

def distribution_to_versions(self) -> dict[str, str]:
return {
distribution: getattr(self, f"{distribution}_version")
for distribution in self.distributions()
if getattr(self, f"no_{distribution}") is False and getattr(self, f"{distribution}_version") != "none"
if getattr(self, f"no_{distribution}", None) is False and getattr(self, f"{distribution}_version") != "none"
}

@classmethod
Expand Down Expand Up @@ -81,6 +85,8 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
for distribution, default in cls.distributions().items():
if interpreter.version_info[:2] >= (3, 12) and distribution == "setuptools":
default = "none" # noqa: PLW2901
if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
continue
parser.add_argument(
f"--{distribution}",
dest=distribution,
Expand All @@ -89,20 +95,16 @@ def add_parser_arguments(cls, parser, interpreter, app_data): # noqa: ARG003
default=default,
)
for distribution in cls.distributions():
help_ = f"do not install {distribution}"
if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
help_ = SUPPRESS
parser.add_argument(
f"--no-{distribution}",
dest=f"no_{distribution}",
action="store_true",
help=f"do not install {distribution}",
help=help_,
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 All @@ -118,7 +120,7 @@ def __repr__(self) -> str:
result += f"extra_search_dir={', '.join(str(i) for i in self.extra_search_dir)},"
result += f"download={self.download},"
for distribution in self.distributions():
if getattr(self, f"no_{distribution}"):
if getattr(self, f"no_{distribution}", None):
continue
version = getattr(self, f"{distribution}_version", None)
if version == "none":
Expand Down
1 change: 1 addition & 0 deletions src/virtualenv/seed/wheels/embed/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"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.1-py3-none-any.whl",
Expand Down
Binary file not shown.
6 changes: 1 addition & 5 deletions tests/unit/create/test_creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,6 @@ 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 Down Expand Up @@ -472,7 +468,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", "--copies", "--setuptools", "bundle"],
[str(tmp_path / "venv"), "--activators", "", "--no-pip", "--no-wheel", "--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
14 changes: 9 additions & 5 deletions tests/unit/seed/embed/test_base_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,25 @@ def test_download_cli_flag(args, download, tmp_path):
assert session.seeder.download is download


# DEPRECATED: Remove in pip 26
@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheels for Python 3.8")
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."
"The --no-wheel option is deprecated. It has no effect for Python >= "
"3.8 as wheel is no longer bundled in virtualenv."
)


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"}
if sys.version_info[:2] >= (3, 12):
expected = {"pip": "bundle"}
elif sys.version_info[:2] >= (3, 9):
expected = {"pip": "bundle", "setuptools": "bundle"}
else:
expected = {"pip": "bundle", "setuptools": "bundle", "wheel": "bundle"}
assert session.seeder.distribution_to_versions() == expected
24 changes: 18 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 @@ -25,7 +25,7 @@

@pytest.mark.slow
@pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True])
def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies):
def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies, for_py_version): # noqa: PLR0915
current = PythonInfo.current_system()
bundle_ver = BUNDLE_SUPPORT[current.version_release_str]
create_cmd = [
Expand All @@ -45,6 +45,8 @@ def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies)
current_fastest,
"-vv",
]
if for_py_version == "3.8":
create_cmd += ["--wheel", bundle_ver["wheel"].split("-")[1]]
if not copies:
create_cmd.append("--symlink-app-data")
result = cli_run(create_cmd)
Expand Down Expand Up @@ -110,7 +112,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"])
process = Popen([*remove_cmd, "pip", "wheel"])
_, __ = process.communicate()
assert not process.returncode
# pip is greedy here, removing all packages removes the site-package too
Expand Down Expand Up @@ -208,13 +210,20 @@ def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest


@pytest.mark.slow
@pytest.mark.parametrize("pkg", ["pip", "setuptools"])
@pytest.mark.parametrize("pkg", ["pip", "setuptools", "wheel"])
@pytest.mark.usefixtures("session_app_data", "current_fastest", "coverage_env")
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg):
def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg, for_py_version):
if for_py_version != "3.8" and pkg == "wheel":
msg = "wheel isn't installed on Python > 3.8"
raise pytest.skip(msg)
create_cmd = [str(tmp_path), "--seeder", "app-data", f"--no-{pkg}", "--setuptools", "bundle"]
if for_py_version == "3.8":
create_cmd += ["--wheel", "bundle"]
result = cli_run(create_cmd)
assert not (result.creator.purelib / pkg).exists()
for key in {"pip", "setuptools"} - {pkg}:
for key in {"pip", "setuptools", "wheel"} - {pkg}:
if for_py_version != "3.8" and key == "wheel":
continue
assert (result.creator.purelib / key).exists()


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

def _run(name):
try:
cli_run(["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"])
cmd = ["--seeder", "app-data", str(tmp_path / name), "--no-setuptools"]
if sys.version_info[:2] == (3, 8):
cmd.append("--no-wheel")
cli_run(cmd)
except Exception as exception: # noqa: BLE001
as_str = str(exception)
exceptions.append(as_str)
Expand Down
9 changes: 7 additions & 2 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", ""])
@pytest.mark.parametrize("no", ["pip", "setuptools", "wheel", ""])
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 @@ -50,6 +50,8 @@ def _execute(cmd, env):
original = PipInvoke._execute # noqa: SLF001
run = mocker.patch.object(PipInvoke, "_execute", side_effect=_execute)
versions = {"pip": "embed", "setuptools": "bundle"}
if sys.version_info[:2] == (3, 8):
versions["wheel"] = new["wheel"].split("-")[1]

create_cmd = [
"--seeder",
Expand All @@ -76,13 +78,16 @@ 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"):
for key in ("pip", "setuptools", "wheel"):
if key == no:
continue
if sys.version_info[:2] >= (3, 9) and key == "wheel":
continue
assert locals()[key] in files_post_first_create
3 changes: 2 additions & 1 deletion 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, "pip": versions}
expected = {"setuptools": versions, "wheel": ["3.8"], "pip": versions}
assert packages == expected


Expand All @@ -97,6 +97,7 @@ def test_pick_periodic_update(tmp_path, mocker, for_py_version):
"--activators",
"",
"--no-periodic-update",
"--no-wheel",
"--no-pip",
"--setuptools",
"bundle",
Expand Down
Loading