diff options
| author | Stefano Rivera <stefanor@debian.org> | 2026-06-19 07:45:18 -0400 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2026-06-19 16:48:34 +0000 |
| commit | 1cd8faed97cde8d961a9b6ff5428df9f09c152f1 (patch) | |
| tree | 52d5b1bc1145ff09da54bcd81c6e8a25ee6599a3 | |
| parent | 462e581734c74fe6a4bcb8e64be8ed60fe994892 (diff) | |
21.5.1+ds-1 (patches unapplied)HEADimport/21.5.1+ds-1ubuntu/stonking-proposedubuntu/stonking-develubuntu/develdebian/sid
Imported using git-ubuntu import.
Notes
Notes:
* New upstream release.
* Refresh patches.
27 files changed, 277 insertions, 155 deletions
@@ -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 @@ -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" ] |
