summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--PKG-INFO8
-rw-r--r--debian/changelog7
-rw-r--r--debian/patches/debian_update_for_available_wheels.patch4
-rw-r--r--debian/patches/debian_wheel_location.patch6
-rw-r--r--debian/patches/disable-periodic-update.patch4
-rw-r--r--debian/patches/wheel-package-error10
-rw-r--r--pyproject.toml20
-rw-r--r--src/virtualenv/activation/bash/activate.sh4
-rw-r--r--src/virtualenv/activation/fish/activate.fish3
-rw-r--r--src/virtualenv/app_data/via_disk_folder.py2
-rw-r--r--src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py19
-rw-r--r--src/virtualenv/run/plugin/seeders.py5
-rw-r--r--src/virtualenv/seed/embed/base_embed.py49
-rw-r--r--src/virtualenv/seed/seeder.py12
-rw-r--r--src/virtualenv/seed/wheels/embed/__init__.py50
-rw-r--r--src/virtualenv/version.py4
-rw-r--r--tasks/upgrade_wheels.py6
-rw-r--r--tests/conftest.py12
-rw-r--r--tests/unit/activation/test_bash.py32
-rw-r--r--tests/unit/create/test_creator.py30
-rw-r--r--tests/unit/seed/embed/test_base_embed.py82
-rw-r--r--tests/unit/seed/embed/test_bootstrap_link_via_app_data.py19
-rw-r--r--tests/unit/seed/embed/test_pip_invoke.py8
-rw-r--r--tests/unit/seed/wheels/test_acquire.py2
-rw-r--r--tests/unit/seed/wheels/test_periodic_update.py4
-rw-r--r--tests/unit/seed/wheels/test_wheels_util.py23
-rw-r--r--tox.toml7
27 files changed, 277 insertions, 155 deletions
diff --git a/PKG-INFO b/PKG-INFO
index f4dda1d..3710edf 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: virtualenv
-Version: 21.4.1
+Version: 21.5.1
Summary: Virtual Python Environment builder
Project-URL: Documentation, https://virtualenv.pypa.io
Project-URL: Homepage, https://github.com/pypa/virtualenv
@@ -17,7 +17,6 @@ Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
@@ -29,13 +28,12 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: Utilities
-Requires-Python: >=3.8
+Requires-Python: >=3.9
Requires-Dist: distlib<1,>=0.3.7
Requires-Dist: filelock<4,>=3.24.2; python_version >= '3.10'
Requires-Dist: filelock<=3.19.1,>=3.16.1; python_version < '3.10'
-Requires-Dist: importlib-metadata>=6.6; python_version < '3.8'
Requires-Dist: platformdirs<5,>=3.9.1
-Requires-Dist: python-discovery>=1.4
+Requires-Dist: python-discovery>=1.4.2
Requires-Dist: typing-extensions>=4.13.2; python_version < '3.11'
Description-Content-Type: text/markdown
diff --git a/debian/changelog b/debian/changelog
index b6bae75..a2dc314 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-virtualenv (21.5.1+ds-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Refresh patches.
+
+ -- Stefano Rivera <stefanor@debian.org> Fri, 19 Jun 2026 07:45:18 -0400
+
python-virtualenv (21.4.1+ds-1) unstable; urgency=medium
* New upstream release.
diff --git a/debian/patches/debian_update_for_available_wheels.patch b/debian/patches/debian_update_for_available_wheels.patch
index 66666e1..62ae3f5 100644
--- a/debian/patches/debian_update_for_available_wheels.patch
+++ b/debian/patches/debian_update_for_available_wheels.patch
@@ -12,10 +12,10 @@ Last-Update: 2026-05-08
1 file changed, 23 insertions(+)
diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py
-index 4566137..7d3ce42 100644
+index 1b5e2a2..77d16e7 100644
--- a/src/virtualenv/seed/wheels/embed/__init__.py
+++ b/src/virtualenv/seed/wheels/embed/__init__.py
-@@ -115,6 +115,29 @@ def _hash_bundled_wheel(path: Path) -> str:
+@@ -123,6 +123,29 @@ def _hash_bundled_wheel(path: Path) -> str:
return digest.hexdigest()
diff --git a/debian/patches/debian_wheel_location.patch b/debian/patches/debian_wheel_location.patch
index c7d26dd..57fb24a 100644
--- a/debian/patches/debian_wheel_location.patch
+++ b/debian/patches/debian_wheel_location.patch
@@ -13,7 +13,7 @@ Last-Update: 2020-07-15
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py
-index 05b359e..4566137 100644
+index ac9a85f..1b5e2a2 100644
--- a/src/virtualenv/seed/wheels/embed/__init__.py
+++ b/src/virtualenv/seed/wheels/embed/__init__.py
@@ -7,7 +7,7 @@ from pathlib import Path
@@ -23,5 +23,5 @@ index 05b359e..4566137 100644
-BUNDLE_FOLDER = Path(__file__).absolute().parent
+BUNDLE_FOLDER = Path('/usr/share/python-wheels')
BUNDLE_SUPPORT = {
- "3.8": {
- "pip": "pip-25.0.1-py3-none-any.whl",
+ "3.9": {
+ "pip": "pip-26.0.1-py3-none-any.whl",
diff --git a/debian/patches/disable-periodic-update.patch b/debian/patches/disable-periodic-update.patch
index 6b45de2..4d16211 100644
--- a/debian/patches/disable-periodic-update.patch
+++ b/debian/patches/disable-periodic-update.patch
@@ -11,10 +11,10 @@ Forwarded: not-needed
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py
-index cb344ce..04b7025 100644
+index 07f15d3..2cca888 100644
--- a/src/virtualenv/seed/embed/base_embed.py
+++ b/src/virtualenv/seed/embed/base_embed.py
-@@ -18,7 +18,7 @@ if TYPE_CHECKING:
+@@ -19,7 +19,7 @@ if TYPE_CHECKING:
from virtualenv.config.cli.parser import VirtualEnvOptions
LOGGER = logging.getLogger(__name__)
diff --git a/debian/patches/wheel-package-error b/debian/patches/wheel-package-error
index c3ae98e..68ceebd 100644
--- a/debian/patches/wheel-package-error
+++ b/debian/patches/wheel-package-error
@@ -8,10 +8,10 @@ Forwarded: not-needed
1 file changed, 14 insertions(+)
diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py
-index 7d3ce42..dab30bc 100644
+index 77d16e7..5eed57e 100644
--- a/src/virtualenv/seed/wheels/embed/__init__.py
+++ b/src/virtualenv/seed/wheels/embed/__init__.py
-@@ -75,6 +75,20 @@ def get_embed_wheel(distribution: str, for_py_version: str) -> Wheel | None:
+@@ -77,6 +77,20 @@ def get_embed_wheel(distribution: str, for_py_version: str | None) -> Wheel | No
:raises RuntimeError: if the bundled wheel on disk fails SHA-256 verification.
"""
@@ -29,6 +29,6 @@ index 7d3ce42..dab30bc 100644
+ ))
+ # End Debian specific
+
- mapping = BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX]
- wheel_file = mapping.get(distribution)
- if wheel_file is None:
+ if for_py_version is None or _release_tuple(for_py_version) > _release_tuple(MAX):
+ # no specific target, or a Python newer than anything bundled: reuse the newest bundle
+ mapping = BUNDLE_SUPPORT[MAX]
diff --git a/pyproject.toml b/pyproject.toml
index 03cc09c..531a662 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,7 +19,7 @@ maintainers = [
{ name = "Bernat Gabor", email = "gaborjbernat@gmail.com" },
{ name = "Rahul Devikar", email = "rahuldevikar5512@gmail.com" },
]
-requires-python = ">=3.8"
+requires-python = ">=3.9"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
@@ -28,7 +28,6 @@ classifiers = [
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
@@ -48,9 +47,8 @@ dependencies = [
"distlib>=0.3.7,<1",
"filelock>=3.16.1,<=3.19.1; python_version<'3.10'",
"filelock>=3.24.2,<4; python_version>='3.10'",
- "importlib-metadata>=6.6; python_version<'3.8'",
"platformdirs>=3.9.1,<5",
- "python-discovery>=1.4",
+ "python-discovery>=1.4.2",
"typing-extensions>=4.13.2; python_version<'3.11'",
]
urls.Documentation = "https://virtualenv.pypa.io"
@@ -136,14 +134,14 @@ pkg-meta = [
]
[tool.hatch]
+version.source = "vcs"
build.hooks.vcs.version-file = "src/virtualenv/version.py"
build.targets.sdist.include = [
"/src",
- "/tests",
"/tasks",
+ "/tests",
"/tox.toml",
]
-version.source = "vcs"
[tool.ruff]
line-length = 120
@@ -215,15 +213,15 @@ max_supported_python = "3.14"
rules.unused-ignore-comment = "ignore" # some ignores are platform/version-specific (e.g., ctypes.windll on Linux)
[tool.pytest]
+ini_options.addopts = "--showlocals --no-success-flaky-report"
ini_options.markers = [
- "slow",
"graalpy: minimal test suite for GraalPy validation",
+ "slow",
]
-ini_options.timeout = 120
-ini_options.addopts = "--showlocals --no-success-flaky-report"
ini_options.env = [
"PYTHONIOENCODING=utf-8",
]
+ini_options.timeout = 120
[tool.coverage]
run.dynamic_context = "test_function"
@@ -253,9 +251,9 @@ html.skip_covered = false
[tool.towncrier]
name = "virtualenv"
-filename = "docs/changelog.rst"
directory = "docs/changelog"
+filename = "docs/changelog.rst"
+template = "docs/changelog/template.jinja2"
title_format = false
issue_format = ":issue:`{issue}`"
-template = "docs/changelog/template.jinja2"
underlines = [ "*", "=", "-" ]
diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh
index b418101..9380bfe 100644
--- a/src/virtualenv/activation/bash/activate.sh
+++ b/src/virtualenv/activation/bash/activate.sh
@@ -41,11 +41,10 @@ deactivate () {
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
- hash -r 2>/dev/null
+ hash -r 2>/dev/null || true
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="$_OLD_VIRTUAL_PS1"
- export PS1
unset _OLD_VIRTUAL_PS1
fi
@@ -116,7 +115,6 @@ fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1-}"
PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}"
- export PS1
fi
# Make sure to unalias pydoc if it's already there
diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish
index 33a8c7f..58a12d7 100644
--- a/src/virtualenv/activation/fish/activate.fish
+++ b/src/virtualenv/activation/fish/activate.fish
@@ -61,6 +61,9 @@ end
deactivate nondestructive
set -gx VIRTUAL_ENV __VIRTUAL_ENV__
+if string match -qr 'CYGWIN|MSYS|MINGW' (uname)
+ set -gx VIRTUAL_ENV (cygpath -u $VIRTUAL_ENV)
+end
set -gx _OLD_PKG_CONFIG_PATH "$PKG_CONFIG_PATH"
set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig:$PKG_CONFIG_PATH"
diff --git a/src/virtualenv/app_data/via_disk_folder.py b/src/virtualenv/app_data/via_disk_folder.py
index 5d75002..78e4e1c 100644
--- a/src/virtualenv/app_data/via_disk_folder.py
+++ b/src/virtualenv/app_data/via_disk_folder.py
@@ -86,7 +86,7 @@ class AppDataDiskFolder(AppData):
@property
def py_info_at(self) -> ReentrantFileLock:
- return self.lock / "py_info" / "4" # ty: ignore[invalid-return-type]
+ return self.lock / "py_info" / "5" # ty: ignore[invalid-return-type]
def py_info(self, path: Path) -> PyInfoStoreDisk:
return PyInfoStoreDisk(self.py_info_at, path)
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
index ccb7f8d..efd02e6 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
@@ -4,7 +4,6 @@ import abc
import fnmatch
from operator import methodcaller as method
from pathlib import Path
-from textwrap import dedent
from typing import TYPE_CHECKING
from virtualenv.create.describe import Python3Supports
@@ -69,24 +68,6 @@ class CPython3Posix(CPythonPosix, CPython3):
ensure_dir(dest.parent)
copy_path(shared_lib, dest)
- def env_patch_text(self) -> str:
- text = super().env_patch_text()
- if self.pyvenv_launch_patch_active(self.interpreter):
- text += dedent(
- """
- # for https://github.com/python/cpython/pull/9516, see https://github.com/pypa/virtualenv/issues/1704
- import os
- if "__PYVENV_LAUNCHER__" in os.environ:
- del os.environ["__PYVENV_LAUNCHER__"]
- """,
- )
- return text
-
- @classmethod
- def pyvenv_launch_patch_active(cls, interpreter: PythonInfo) -> bool: # drop when Python 3.8 support is dropped
- ver = interpreter.version_info
- return interpreter.platform == "darwin" and (3, 8, 3) > ver >= (3, 8)
-
class CPython3Windows(CPythonWindows, CPython3):
"""CPython 3 on Windows."""
diff --git a/src/virtualenv/run/plugin/seeders.py b/src/virtualenv/run/plugin/seeders.py
index 377b58e..128a985 100644
--- a/src/virtualenv/run/plugin/seeders.py
+++ b/src/virtualenv/run/plugin/seeders.py
@@ -43,7 +43,10 @@ class SeederSelector(ComponentBuilder):
def create(self, options: VirtualEnvOptions) -> Seeder:
assert self._impl_class is not None # noqa: S101 # Set by handle_selected_arg_parse
- return self._impl_class(options)
+ seeder = self._impl_class(options)
+ if seeder.enabled and (reason := seeder.cannot_seed(self.interpreter)) is not None:
+ raise RuntimeError(reason)
+ return seeder
__all__ = [
diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py
index cb344ce..07f15d3 100644
--- a/src/virtualenv/seed/embed/base_embed.py
+++ b/src/virtualenv/seed/embed/base_embed.py
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
from virtualenv.seed.seeder import Seeder
from virtualenv.seed.wheels import Version
+from virtualenv.seed.wheels.embed import MIN, OLDEST_SUPPORTED
if TYPE_CHECKING:
from argparse import ArgumentParser
@@ -31,25 +32,21 @@ class BaseEmbed(Seeder, ABC):
self.pip_version = options.pip
self.setuptools_version = options.setuptools
- # wheel version needs special handling
- # on Python >= 3.9, the default is None (as in not used)
- # so we can differentiate between explicit and implicit none
+ # virtualenv no longer bundles wheel; the parsed default stays None (unused) so the
+ # warning below fires only when you pass --wheel or --no-wheel
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.9 as wheel is no longer "
- "bundled in virtualenv.",
- )
- self.no_wheel = True
+ if options.wheel is not None or options.no_wheel:
+ LOGGER.warning(
+ "DEPRECATION: the --wheel and --no-wheel options do nothing; virtualenv no longer bundles wheel. "
+ "They will be removed in a release after 2026-12. Stop passing them.",
+ )
+ self.no_wheel = True
if not self.distribution_to_versions():
self.enabled = False
@@ -70,6 +67,28 @@ class BaseEmbed(Seeder, ABC):
}
@classmethod
+ def cannot_seed(cls, interpreter: PythonInfo) -> str | None:
+ """Explain why the bundled wheels cannot seed the target Python version.
+
+ The embedded pip/setuptools stopped shipping for Pythons below :data:`OLDEST_SUPPORTED`, so seeding one would
+ install an incompatible wheel.
+
+ :param interpreter: the interpreter to be seeded
+
+ :returns: ``None`` when the bundled wheels still support the target, otherwise a message naming the target and
+ the remedies
+
+ """
+ if interpreter.version_info[:2] >= OLDEST_SUPPORTED:
+ return None
+ target = f"{interpreter.version_info.major}.{interpreter.version_info.minor}"
+ return (
+ f"the bundled seeder no longer ships pip/setuptools for Python {target}; the oldest supported target is "
+ f"Python {MIN} - pass --no-seed for an empty environment, use a seeder that provides Python {target} "
+ f"wheels, or install an older virtualenv release"
+ )
+
+ @classmethod
def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData) -> None: # noqa: ARG003
group = parser.add_mutually_exclusive_group()
group.add_argument(
@@ -97,9 +116,9 @@ class BaseEmbed(Seeder, ABC):
)
for distribution, default in cls.distributions().items():
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"}:
+ if interpreter.version_info[:2] >= (3, 12) and distribution == "setuptools":
default = "none" # noqa: PLW2901
- if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
+ if distribution == "wheel":
default = None # noqa: PLW2901
help_ = SUPPRESS
parser.add_argument(
@@ -111,7 +130,7 @@ class BaseEmbed(Seeder, ABC):
)
for distribution in cls.distributions():
help_ = f"do not install {distribution}"
- if interpreter.version_info[:2] >= (3, 9) and distribution == "wheel":
+ if distribution == "wheel":
help_ = SUPPRESS
parser.add_argument(
f"--no-{distribution}",
diff --git a/src/virtualenv/seed/seeder.py b/src/virtualenv/seed/seeder.py
index 4ae6276..01e3128 100644
--- a/src/virtualenv/seed/seeder.py
+++ b/src/virtualenv/seed/seeder.py
@@ -27,6 +27,18 @@ class Seeder(ABC):
self.env = options.env
@classmethod
+ def cannot_seed(cls, interpreter: PythonInfo) -> str | None: # noqa: ARG003
+ """Explain why this seeder cannot install seed packages for the given interpreter.
+
+ :param interpreter: the interpreter the environment is based on
+
+ :returns: ``None`` when the seeder supports the interpreter, otherwise a message describing why it cannot;
+ selection rejects a seeder that returns a message and surfaces it to the user
+
+ """
+ return None
+
+ @classmethod
def add_parser_arguments(cls, parser: ArgumentParser, interpreter: PythonInfo, app_data: AppData) -> None:
"""Add CLI arguments for this seed mechanisms.
diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py
index 05b359e..ac9a85f 100644
--- a/src/virtualenv/seed/wheels/embed/__init__.py
+++ b/src/virtualenv/seed/wheels/embed/__init__.py
@@ -9,73 +9,81 @@ from virtualenv.seed.wheels.util import Wheel
BUNDLE_FOLDER = Path(__file__).absolute().parent
BUNDLE_SUPPORT = {
- "3.8": {
- "pip": "pip-25.0.1-py3-none-any.whl",
- "setuptools": "setuptools-75.3.4-py3-none-any.whl",
- "wheel": "wheel-0.45.1-py3-none-any.whl",
- },
"3.9": {
"pip": "pip-26.0.1-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.10": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.11": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.12": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.13": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.14": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.15": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
"3.16": {
- "pip": "pip-26.1.1-py3-none-any.whl",
+ "pip": "pip-26.1.2-py3-none-any.whl",
"setuptools": "setuptools-82.0.1-py3-none-any.whl",
},
}
MAX = next(reversed(BUNDLE_SUPPORT))
+MIN = next(iter(BUNDLE_SUPPORT))
+
+
+def _release_tuple(version: str) -> tuple[int, ...]:
+ return tuple(int(part) for part in version.split("."))
+
+
+# oldest target Python version virtualenv still bundles seed wheels for; anything below this has no embedded pip
+OLDEST_SUPPORTED = _release_tuple(MIN)
# SHA-256 of every bundled wheel. Verified on load so a corrupted or tampered wheel on disk fails loud instead of
# being handed to pip. Generated together with ``BUNDLE_SUPPORT`` by ``tasks/upgrade_wheels.py``.
BUNDLE_SHA256 = {
- "pip-25.0.1-py3-none-any.whl": "c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f",
"pip-26.0.1-py3-none-any.whl": "bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b",
- "pip-26.1.1-py3-none-any.whl": "99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb",
- "setuptools-75.3.4-py3-none-any.whl": "2dd50a7f42dddfa1d02a36f275dbe716f38ed250224f609d35fb60a09593d93e",
+ "pip-26.1.2-py3-none-any.whl": "382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab",
"setuptools-82.0.1-py3-none-any.whl": "a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb",
- "wheel-0.45.1-py3-none-any.whl": "708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248",
}
_VERIFIED_WHEELS: set[str] = set()
-def get_embed_wheel(distribution: str, for_py_version: str) -> Wheel | None:
+def get_embed_wheel(distribution: str, for_py_version: str | None) -> Wheel | None:
"""Return the bundled wheel that ships with virtualenv for a given distribution and Python version.
:param distribution: project name of the seed package, for example ``pip`` or ``setuptools``.
- :param for_py_version: major.minor Python version string the environment will be created for.
+ :param for_py_version: major.minor Python version string the environment will be created for, or ``None`` to use the
+ newest bundle.
:returns: a :class:`Wheel` pointing at the verified bundled file, or ``None`` when no wheel is bundled for the
- requested combination.
+ requested combination, including target versions below the oldest bundled one.
:raises RuntimeError: if the bundled wheel on disk fails SHA-256 verification.
"""
- mapping = BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX]
+ if for_py_version is None or _release_tuple(for_py_version) > _release_tuple(MAX):
+ # no specific target, or a Python newer than anything bundled: reuse the newest bundle
+ mapping = BUNDLE_SUPPORT[MAX]
+ else: # versions below the oldest bundled one fall through to None instead of an incompatible newer wheel
+ mapping = BUNDLE_SUPPORT.get(for_py_version)
+ if not mapping:
+ return None
wheel_file = mapping.get(distribution)
if wheel_file is None:
return None
@@ -120,5 +128,7 @@ __all__ = [
"BUNDLE_SHA256",
"BUNDLE_SUPPORT",
"MAX",
+ "MIN",
+ "OLDEST_SUPPORTED",
"get_embed_wheel",
]
diff --git a/src/virtualenv/version.py b/src/virtualenv/version.py
index 7c73228..a4933e5 100644
--- a/src/virtualenv/version.py
+++ b/src/virtualenv/version.py
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
commit_id: str | None
__commit_id__: str | None
-__version__ = version = '21.4.1'
-__version_tuple__ = version_tuple = (21, 4, 1)
+__version__ = version = '21.5.1'
+__version_tuple__ = version_tuple = (21, 5, 1)
__commit_id__ = commit_id = None
diff --git a/tasks/upgrade_wheels.py b/tasks/upgrade_wheels.py
index acff727..4a315c5 100644
--- a/tasks/upgrade_wheels.py
+++ b/tasks/upgrade_wheels.py
@@ -17,8 +17,8 @@ from typing import NoReturn
STRICT = "UPGRADE_ADVISORY" not in os.environ
-BUNDLED = ["pip", "setuptools", "wheel"]
-SUPPORT = [(3, i) for i in range(8, 17)]
+BUNDLED = ["pip", "setuptools"]
+SUPPORT = [(3, i) for i in range(9, 17)]
DEST = Path(__file__).resolve().parents[1] / "src" / "virtualenv" / "seed" / "wheels" / "embed"
@@ -51,8 +51,6 @@ def _download_all(temp_path: Path) -> dict[Path, str]:
into.mkdir()
folders[into] = support_ver
for package in BUNDLED:
- if package == "wheel" and support >= (3, 9):
- continue
thread = Thread(target=download, args=(support_ver, str(into), package))
targets.append(thread)
thread.start()
diff --git a/tests/conftest.py b/tests/conftest.py
index 5b42553..17b059c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -98,7 +98,7 @@ def link(link_folder, link_file):
return _link
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _ensure_logging_stable():
logger_level = LOGGER.level
handlers = list(LOGGER.handlers)
@@ -113,7 +113,7 @@ def _ensure_logging_stable():
LOGGER.setLevel(logger_level)
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _check_cwd_not_changed_by_test():
old = os.getcwd()
yield
@@ -122,7 +122,7 @@ def _check_cwd_not_changed_by_test():
pytest.fail(f"tests changed cwd: {old!r} => {new!r}")
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _ensure_py_info_cache_empty(session_app_data):
PythonInfo.clear_cache(session_app_data)
yield
@@ -141,7 +141,7 @@ def change_os_environ(key, value):
os.environ[env_var] = previous
-@pytest.fixture(autouse=True, scope="session")
+@pytest.fixture(autouse=True, scope="session") # noqa: RUF076
def _ignore_global_config(tmp_path_factory):
filename = str(tmp_path_factory.mktemp("folder") / "virtualenv-test-suite.ini")
with change_os_environ("VIRTUALENV_CONFIG_FILE", filename):
@@ -166,7 +166,7 @@ def _assert_environ_stable(old: dict[str, str], to_clean: set[str]) -> None:
pytest.fail(msg)
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _check_os_environ_stable():
old = os.environ.copy()
# ensure we don't inherit parent env variables
@@ -198,7 +198,7 @@ COV_ENV_VAR = "COVERAGE_PROCESS_START"
COVERAGE_RUN = os.environ.get(str(COV_ENV_VAR))
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def coverage_env(monkeypatch, link, request):
"""Enable coverage report collection on the created virtual environments by injecting the coverage project"""
if COVERAGE_RUN and "_no_coverage" not in request.fixturenames:
diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py
index 7a1bd35..89b0544 100644
--- a/tests/unit/activation/test_bash.py
+++ b/tests/unit/activation/test_bash.py
@@ -1,7 +1,9 @@
from __future__ import annotations
+import shlex
import shutil
import subprocess
+import sys
from argparse import Namespace
import pytest
@@ -102,6 +104,34 @@ def test_bash_activate_relocation_resolves_virtual_env(tmp_path, current_fastest
@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash")
+def test_bash_activate_does_not_export_ps1(tmp_path, current_fastest) -> None:
+ dest = tmp_path / "env"
+ cli_run([
+ "--without-pip",
+ str(dest),
+ "--creator",
+ current_fastest,
+ "--no-periodic-update",
+ "--activators",
+ "bash",
+ ])
+ activate_script = dest / "bin" / "activate"
+ print_ps1 = f"{shlex.quote(sys.executable)} -c 'import os; print(os.environ.get(\"PS1\"))'"
+ result = subprocess.run(
+ [
+ "bash",
+ "-c",
+ (f"unset PS1; PS1='base$ '; source \"{activate_script}\" && {print_ps1} && deactivate && {print_ps1}"),
+ ],
+ capture_output=True,
+ encoding="utf-8",
+ text=True,
+ )
+ assert result.returncode == 0, result.stderr
+ assert result.stdout.splitlines() == ["None", "None"]
+
+
+@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash")
@pytest.mark.parametrize("hashing_enabled", [True, False])
def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester) -> None:
class Bash(raise_on_non_source_class):
@@ -121,6 +151,6 @@ def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester) ->
return super().activate_call(script) + " || exit 1"
def print_prompt(self):
- return self.print_os_env_var("PS1")
+ return 'printf "%s\\n" "$PS1"'
activation_tester(Bash)
diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py
index 3a8ad25..6d463be 100644
--- a/tests/unit/create/test_creator.py
+++ b/tests/unit/create/test_creator.py
@@ -28,7 +28,6 @@ from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info
from virtualenv.create.pyenv_cfg import PyEnvCfg
from virtualenv.create.via_global_ref import api
from virtualenv.create.via_global_ref.builtin.cpython.common import is_mac_os_framework, is_macos_brew
-from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix
from virtualenv.info import IS_PYPY, IS_WIN, fs_is_case_sensitive
from virtualenv.run import cli_run, session_via_cli
from virtualenv.run.plugin.creators import CreatorSelector
@@ -212,13 +211,6 @@ def test_create_no_seed( # noqa: C901, PLR0912, PLR0913, PLR0915
assert python_w.exists()
assert python_w.read_bytes() != creator.exe.read_bytes()
- if CPython3Posix.pyvenv_launch_patch_active(PythonInfo.from_exe(python)) and creator_key != "venv":
- result = subprocess.check_output(
- [str(creator.exe), "-c", 'import os; print(os.environ.get("__PYVENV_LAUNCHER__"))'],
- text=True,
- ).strip()
- assert result == "None"
-
git_ignore = (dest / ".gitignore").read_text(encoding="utf-8")
assert git_ignore.splitlines() == ["# created by virtualenv automatically", "*"]
@@ -385,6 +377,28 @@ def test_home_path_is_exe_parent(tmp_path, creator) -> None:
assert any(os.path.exists(os.path.join(cfg["home"], exe)) for exe in exes)
+@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
+@pytest.mark.skipif(
+ bool(CURRENT.sysconfig_vars.get("PYTHONFRAMEWORK")),
+ reason="framework builds self-locate via dyld and keep the recorded path",
+)
+@pytest.mark.usefixtures("temp_app_data")
+def test_home_resolves_executable_only_symlink(tmp_path: Path) -> None:
+ """An executable-only symlink must not be recorded as home / base-executable (issue #3157)."""
+ system_exe = CURRENT.system_executable
+ assert system_exe is not None
+ link = tmp_path / "symdir" / "python3"
+ link.parent.mkdir()
+ link.symlink_to(system_exe)
+
+ result = cli_run(["-p", str(link), str(tmp_path / "env"), "--seeder", "app-data", "--without-pip"])
+ cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path)
+
+ assert Path(cfg["home"]) != link.parent
+ assert os.path.samefile(cfg["base-executable"], system_exe)
+ assert Path(cfg["home"]) == Path(cfg["base-executable"]).parent
+
+
@pytest.mark.usefixtures("temp_app_data")
def test_create_parallel(tmp_path) -> None:
def create(count) -> None:
diff --git a/tests/unit/seed/embed/test_base_embed.py b/tests/unit/seed/embed/test_base_embed.py
index 4dd8375..522c185 100644
--- a/tests/unit/seed/embed/test_base_embed.py
+++ b/tests/unit/seed/embed/test_base_embed.py
@@ -1,14 +1,24 @@
from __future__ import annotations
import sys
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, NamedTuple
import pytest
from virtualenv.run import session_via_cli
+from virtualenv.seed.embed.pip_invoke import PipInvoke
+from virtualenv.seed.embed.via_app_data.via_app_data import FromAppData
+from virtualenv.seed.seeder import Seeder
+from virtualenv.seed.wheels.embed import MIN, OLDEST_SUPPORTED
if TYPE_CHECKING:
+ from collections.abc import Callable
from pathlib import Path
+ from unittest.mock import MagicMock
+
+ from pytest_mock import MockerFixture
+
+ from virtualenv.seed.embed.base_embed import BaseEmbed
@pytest.mark.parametrize(
@@ -20,7 +30,6 @@ def test_download_cli_flag(args, download, tmp_path) -> None:
assert session.seeder.download is download
-@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) -> None:
session = session_via_cli([flag, str(tmp_path)])
@@ -31,35 +40,74 @@ def test_wheel_cli_flags_do_nothing(tmp_path, flag) -> None:
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) -> None:
session_via_cli([flag, str(tmp_path)])
out, err = capsys.readouterr()
- assert "The --no-wheel and --wheel options are deprecated." in out + err
+ assert "the --wheel and --no-wheel options do nothing" 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) -> None:
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) -> None:
- session_via_cli([flag, str(tmp_path)])
- out, err = capsys.readouterr()
- assert "The --no-wheel and --wheel options are deprecated." not in out + err
+ assert "the --wheel and --no-wheel options do nothing" not in out + err
def test_embed_wheel_versions(tmp_path: Path) -> None:
session = session_via_cli([str(tmp_path)])
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"}
+ expected = {"pip": "bundle", "setuptools": "bundle"}
assert session.seeder.distribution_to_versions() == expected
+
+
+BUNDLED_SEEDERS = [pytest.param(FromAppData, id="app-data"), pytest.param(PipInvoke, id="pip")]
+
+
+@pytest.mark.parametrize("seeder", BUNDLED_SEEDERS)
+def test_bundled_seeder_reports_reason_below_floor(
+ seeder: type[BaseEmbed], at_version: Callable[[int, int], MagicMock]
+) -> None:
+ reason = seeder.cannot_seed(at_version(3, 8))
+ assert reason is not None
+ assert "Python 3.8" in reason
+ assert f"Python {MIN}" in reason
+ assert "--no-seed" in reason
+
+
+@pytest.mark.parametrize("seeder", BUNDLED_SEEDERS)
+def test_bundled_seeder_has_no_reason_on_oldest_supported(
+ seeder: type[BaseEmbed], at_version: Callable[[int, int], MagicMock]
+) -> None:
+ assert seeder.cannot_seed(at_version(*OLDEST_SUPPORTED)) is None
+
+
+def test_base_seeder_never_blocks(at_version: Callable[[int, int], MagicMock]) -> None:
+ assert Seeder.cannot_seed(at_version(3, 8)) is None
+
+
+def test_selection_surfaces_the_seeder_reason(tmp_path: Path, mocker: MockerFixture) -> None:
+ mocker.patch.object(FromAppData, "cannot_seed", return_value="seeder said no for a specific reason")
+ with pytest.raises(RuntimeError, match="seeder said no for a specific reason"):
+ session_via_cli([str(tmp_path)])
+
+
+def test_no_seed_bypasses_capability_check(tmp_path: Path, mocker: MockerFixture) -> None:
+ mocker.patch.object(FromAppData, "cannot_seed", return_value="seeder said no")
+ assert session_via_cli(["--no-seed", str(tmp_path)]).seeder.enabled is False
+
+
+@pytest.fixture
+def at_version(mocker: MockerFixture) -> Callable[[int, int], MagicMock]:
+ def build(major: int, minor: int) -> MagicMock:
+ interpreter = mocker.MagicMock()
+ interpreter.version_info = _VersionInfo(major, minor)
+ return interpreter
+
+ return build
+
+
+class _VersionInfo(NamedTuple):
+ major: int
+ minor: int
diff --git a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
index 99b6cef..8957f3b 100644
--- a/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
+++ b/tests/unit/seed/embed/test_bootstrap_link_via_app_data.py
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
@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, for_py_version) -> None: # noqa: PLR0915
+def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies) -> None:
current = PythonInfo.current_system()
bundle_ver = BUNDLE_SUPPORT[current.version_release_str]
create_cmd = [
@@ -47,8 +47,6 @@ 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)
@@ -212,20 +210,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, for_py_version) -> None:
- if for_py_version != "3.8" and pkg == "wheel":
- msg = "wheel isn't installed on Python > 3.8"
- raise pytest.skip(msg)
+def test_base_bootstrap_link_via_app_data_no(tmp_path, pkg) -> None:
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", "wheel"} - {pkg}:
- if for_py_version != "3.8" and key == "wheel":
- continue
+ for key in {"pip", "setuptools"} - {pkg}:
assert (result.creator.purelib / key).exists()
@@ -251,8 +242,6 @@ def _run_parallel_threads(tmp_path):
def _run(name) -> None:
try:
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)
diff --git a/tests/unit/seed/embed/test_pip_invoke.py b/tests/unit/seed/embed/test_pip_invoke.py
index 6753fc2..7a721e1 100644
--- a/tests/unit/seed/embed/test_pip_invoke.py
+++ b/tests/unit/seed/embed/test_pip_invoke.py
@@ -25,7 +25,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_f
def _load_embed_wheel(app_data, distribution, _for_py_version, version):
return load_embed_wheel(app_data, distribution, old_ver, version)
- old_ver = "3.8"
+ old_ver = "3.9"
old = BUNDLE_SUPPORT[old_ver]
mocker.patch("virtualenv.seed.wheels.bundle.load_embed_wheel", side_effect=_load_embed_wheel)
@@ -50,8 +50,6 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_f
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",
@@ -85,9 +83,7 @@ def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env, mocker, current_f
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
- if sys.version_info[:2] >= (3, 9) and key == "wheel":
- continue
assert locals()[key] in files_post_first_create
diff --git a/tests/unit/seed/wheels/test_acquire.py b/tests/unit/seed/wheels/test_acquire.py
index f0a2393..f877a4e 100644
--- a/tests/unit/seed/wheels/test_acquire.py
+++ b/tests/unit/seed/wheels/test_acquire.py
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
from pytest_mock import MockerFixture
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _fake_release_date(mocker) -> None:
mocker.patch("virtualenv.seed.wheels.periodic_update.release_date_for_wheel_path", return_value=None)
diff --git a/tests/unit/seed/wheels/test_periodic_update.py b/tests/unit/seed/wheels/test_periodic_update.py
index 33524ce..06ae354 100644
--- a/tests/unit/seed/wheels/test_periodic_update.py
+++ b/tests/unit/seed/wheels/test_periodic_update.py
@@ -33,7 +33,7 @@ from virtualenv.seed.wheels.periodic_update import (
from virtualenv.util.subprocess import CREATE_NO_WINDOW
-@pytest.fixture(autouse=True)
+@pytest.fixture(autouse=True) # noqa: RUF076
def _clear_pypi_info_cache() -> None:
from virtualenv.seed.wheels.periodic_update import _PYPI_CACHE # noqa: PLC0415
@@ -67,7 +67,7 @@ def test_manual_upgrade(session_app_data, caplog, mocker, for_py_version) -> Non
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": ["3.8"], "pip": versions}
+ expected = {"setuptools": versions, "pip": versions}
assert packages == expected
diff --git a/tests/unit/seed/wheels/test_wheels_util.py b/tests/unit/seed/wheels/test_wheels_util.py
index f177a7d..ca88cb3 100644
--- a/tests/unit/seed/wheels/test_wheels_util.py
+++ b/tests/unit/seed/wheels/test_wheels_util.py
@@ -2,17 +2,36 @@ from __future__ import annotations
import pytest
-from virtualenv.seed.wheels.embed import MAX, get_embed_wheel
+from virtualenv.seed.wheels.embed import MAX, MIN, get_embed_wheel
from virtualenv.seed.wheels.util import Wheel
+@pytest.mark.parametrize(
+ "distribution",
+ [pytest.param("pip", id="pip"), pytest.param("setuptools", id="setuptools")],
+)
+def test_embed_wheel_below_oldest_supported_is_missing(distribution: str) -> None:
+ assert get_embed_wheel(distribution, "3.8") is None
+
+
+def test_embed_wheel_oldest_supported_is_present() -> None:
+ assert get_embed_wheel("pip", MIN) is not None
+
+
+def test_embed_wheel_future_version_reuses_newest() -> None:
+ future, newest = get_embed_wheel("pip", "3.99"), get_embed_wheel("pip", MAX)
+ assert future is not None
+ assert newest is not None
+ assert future.name == newest.name
+
+
def test_wheel_support_no_python_requires(mocker) -> None:
wheel = get_embed_wheel("setuptools", for_py_version=None)
zip_mock = mocker.MagicMock()
mocker.patch("virtualenv.seed.wheels.util.ZipFile", new=zip_mock)
zip_mock.return_value.__enter__.return_value.read = lambda _name: b""
- supports = wheel.support_py("3.8")
+ supports = wheel.support_py("3.9")
assert supports is True
diff --git a/tox.toml b/tox.toml
index a0158f5..4b21f21 100644
--- a/tox.toml
+++ b/tox.toml
@@ -6,7 +6,6 @@ env_list = [
"3.11",
"3.10",
"3.9",
- "3.8",
"pypy3",
"3.13t",
"3.14t",
@@ -181,7 +180,7 @@ deps = [ "packaging>=25" ]
commands = [ [ "python", "tasks/make_zipapp.py" ] ]
uv_seed = true
-[env."type-3.8"]
-description = "run type checker (ty) against Python 3.8"
-commands = [ [ "python", "-m", "ty", "check", "src/virtualenv", "--python-version", "3.8" ] ]
+[env."type-3.9"]
+description = "run type checker (ty) against Python 3.9"
+commands = [ [ "python", "-m", "ty", "check", "src/virtualenv", "--python-version", "3.9" ] ]
base = [ "type" ]