Repository: pypa/virtualenv
Pull request #2884 (2025-05-06 15:15:16)

Reintroduce the --wheel CLI option, even though it has no effect on Python > 3.8


Patch:
diff --git a/src/virtualenv/run/__init__.py b/src/virtualenv/run/__init__.py
index 48647ec7..6d54eb1e 100644
--- a/src/virtualenv/run/__init__.py
+++ b/src/virtualenv/run/__init__.py
@@ -48,6 +48,7 @@ def session_via_cli(args, options=None, setup_logging=True, env=None):  # noqa:
     env = os.environ if env is None else env
     parser, elements = build_parser(args, options, setup_logging, env)
     options = parser.parse_args(args)
+    options.py_version = parser._interpreter.version_info  # noqa: SLF001
     creator, seeder, activators = tuple(e.create(options) for e in elements)  # create types
     return Session(
         options.verbosity,
diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py
index 6a02aef5..72fc5a34 100644
--- a/src/virtualenv/seed/embed/base_embed.py
+++ b/src/virtualenv/seed/embed/base_embed.py
@@ -1,13 +1,14 @@
 from __future__ import annotations
 
+import logging
 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
 
+LOGGER = logging.getLogger(__name__)
 PERIODIC_UPDATE_ON_BY_DEFAULT = True
 
 
@@ -20,24 +21,27 @@ class BaseEmbed(Seeder, ABC):
 
         self.pip_version = options.pip
         self.setuptools_version = options.setuptools
-        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 for Python >= 3.8 as wheel is no longer "
-                "bundled in virtualenv.",
-                DeprecationWarning,
-                stacklevel=1,
-            )
+
+        # wheel version needs special handling
+        # on Python > 3.8, the default is None (as in not used)
+        # so we can differentiate between explicit and implicit none
+        self.wheel_version = options.wheel or "none"
 
         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.py_version[:2] >= (3, 9):
+            if options.wheel is not None or options.no_wheel:
+                LOGGER.warning(
+                    "The --no-wheel and --wheel options are deprecated. "
+                    "They have no effect for Python > 3.8 as wheel is no longer "
+                    "bundled in virtualenv.",
+                )
+            self.no_wheel = True
+
         if not self.distribution_to_versions():
             self.enabled = False
 
@@ -83,15 +87,17 @@ class BaseEmbed(Seeder, ABC):
             default=[],
         )
         for distribution, default in cls.distributions().items():
-            if interpreter.version_info[:2] >= (3, 12) and distribution == "setuptools":
+            help_ = f"version of {distribution} to install as seed: embed, bundle, none or exact version"
+            if interpreter.version_info[:2] >= (3, 12) and distribution in {"wheel", "setuptools"}:
                 default = "none"  # noqa: PLW2901
             if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
-                continue
+                default = None  # noqa: PLW2901
+                help_ = SUPPRESS
             parser.add_argument(
                 f"--{distribution}",
                 dest=distribution,
                 metavar="version",
-                help=f"version of {distribution} to install as seed: embed, bundle, none or exact version",
+                help=help_,
                 default=default,
             )
         for distribution in cls.distributions():


Test patch:
diff --git a/tests/unit/seed/embed/test_base_embed.py b/tests/unit/seed/embed/test_base_embed.py
index 14a82165..4f4fadac 100644
--- a/tests/unit/seed/embed/test_base_embed.py
+++ b/tests/unit/seed/embed/test_base_embed.py
@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import sys
-import warnings
 from typing import TYPE_CHECKING
 
 import pytest
@@ -21,17 +20,38 @@ def test_download_cli_flag(args, download, tmp_path):
     assert session.seeder.download is download
 
 
-@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 for Python >= "
-        "3.8 as wheel is no longer bundled in virtualenv."
-    )
+@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8")
+@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"])
+def test_wheel_cli_flags_do_nothing(tmp_path, flag):
+    session = session_via_cli([flag, str(tmp_path)])
+    if sys.version_info[:2] >= (3, 12):
+        expected = {"pip": "bundle"}
+    else:
+        expected = {"pip": "bundle", "setuptools": "bundle"}
+    assert session.seeder.distribution_to_versions() == expected
+
+
+@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8")
+@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"])
+def test_wheel_cli_flags_warn(tmp_path, flag, capsys):
+    session_via_cli([flag, str(tmp_path)])
+    out, err = capsys.readouterr()
+    assert "The --no-wheel and --wheel options are deprecated." in out + err
+
+
+@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason="We still bundle wheel for Python 3.8")
+def test_unused_wheel_cli_flags_dont_warn(tmp_path, capsys):
+    session_via_cli([str(tmp_path)])
+    out, err = capsys.readouterr()
+    assert "The --no-wheel and --wheel options are deprecated." not in out + err
+
+
+@pytest.mark.skipif(sys.version_info[:2] != (3, 8), reason="We only bundle wheel for Python 3.8")
+@pytest.mark.parametrize("flag", ["--no-wheel", "--wheel=none", "--wheel=embed", "--wheel=bundle"])
+def test_wheel_cli_flags_dont_warn_on_38(tmp_path, flag, capsys):
+    session_via_cli([flag, str(tmp_path)])
+    out, err = capsys.readouterr()
+    assert "The --no-wheel and --wheel options are deprecated." not in out + err
 
 
 def test_embed_wheel_versions(tmp_path: Path) -> None: