src/virtualenv/seed/embed/base_embed.py | 2 +- src/virtualenv/seed/wheels/embed/__init__.py | 117 ++++++++++++++++++++- tests/integration/test_run_int.py | 4 + tests/integration/test_zipapp.py | 5 + .../seed/embed/test_bootstrap_link_via_app_data.py | 3 + tests/unit/seed/wheels/test_acquire_find_wheel.py | 20 ++++ 6 files changed, 148 insertions(+), 3 deletions(-) diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py index 5ff2c84..ac18334 100644 --- a/src/virtualenv/seed/embed/base_embed.py +++ b/src/virtualenv/seed/embed/base_embed.py @@ -6,7 +6,7 @@ from pathlib import Path from virtualenv.seed.seeder import Seeder from virtualenv.seed.wheels import Version -PERIODIC_UPDATE_ON_BY_DEFAULT = True +PERIODIC_UPDATE_ON_BY_DEFAULT = False class BaseEmbed(Seeder, metaclass=ABCMeta): diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py index 0d87e4b..e883c03 100644 --- a/src/virtualenv/seed/wheels/embed/__init__.py +++ b/src/virtualenv/seed/wheels/embed/__init__.py @@ -2,9 +2,14 @@ from __future__ import annotations from pathlib import Path +from operator import attrgetter +import re +import sys +import sysconfig +import subprocess + from virtualenv.seed.wheels.util import Wheel -BUNDLE_FOLDER = Path(__file__).absolute().parent BUNDLE_SUPPORT = { "3.7": { "pip": "pip-23.2.1-py3-none-any.whl", @@ -40,8 +45,116 @@ BUNDLE_SUPPORT = { MAX = "3.7" +def get_bundle_folder(for_py_version=None): + """Return path of the system seed wheels""" + global _BUNDLE_FOLDER_CACHE + + try: + return _BUNDLE_FOLDER_CACHE[for_py_version] + except KeyError: + pass + + version_pattern = r"^\d+\.\d+$" + if for_py_version and not re.match(version_pattern, for_py_version): + raise ValueError( + f"Unsupported value for for_py_version: '{for_py_version}', " + f"expected regex: '{version_pattern}'" + ) + if for_py_version is None: + for_py_version_major = str(sys.version_info.major) + else: + for_py_version_major = for_py_version[0] + + # assume all system Pythons were configured with the same "scripts" path + system_python = Path(sysconfig.get_path(name="scripts")) / ( + f"python{for_py_version_major}" + ) + cmd = [ + system_python, + "-c", + ( + "import os, sys, system_seed_wheels;" + "sys.stdout.write(os.path.dirname(system_seed_wheels.__file__))" + ), + ] + try: + _BUNDLE_FOLDER_CACHE[for_py_version] = Path( + subprocess.check_output(cmd, text=True) + ) + return _BUNDLE_FOLDER_CACHE[for_py_version] + except subprocess.CalledProcessError as e: + raise RuntimeError( + "Cannot find the folder with system seed wheels. " + "Please, install system-seed-wheels package if " + f"it has support for your Python interpreter: '{system_python}'" + ) from e + + +class BundleSupport: + SEED_NAMES = ("pip", "setuptools", "wheel") + + def __init__(self, for_py_version=None): + self._system_seed_wheels = None + if for_py_version is None: + self.for_py_version = "{}.{}".format(*sys.version_info[0:2]) + else: + self.for_py_version = for_py_version + self.wheels_dir = get_bundle_folder(for_py_version) + + def discover_wheel(self, distribution): + """based on virtualenv.seed.wheels.util.discover_wheels""" + wheels = [] + for filename in self.wheels_dir.iterdir(): + wheel = Wheel.from_path(filename) + if wheel and wheel.distribution == distribution: + wheels.append(wheel) + sorted_wheels = sorted( + wheels, + key=attrgetter("version_tuple", "distribution"), + reverse=True, + ) + if not sorted_wheels: + raise RuntimeError( + f"Wheel for distribution: '{distribution}' not found on path: " + f"'{self.wheels_dir}'" + ) + return sorted_wheels[0].name + + @property + def system_seed_wheels(self): + if self._system_seed_wheels is None: + wheels_names = {} + for distr in self.SEED_NAMES: + wheels_names[distr] = self.discover_wheel(distr) + self._system_seed_wheels = { + self.for_py_version: dict(wheels_names) + } + + return self._system_seed_wheels + + def __getitem__(self, key): + return self.system_seed_wheels[self.for_py_version] + + def get(self, key, default=None): + return self.system_seed_wheels[self.for_py_version] + + def items(self): + return self.system_seed_wheels.items() + + def keys(self): + return self.system_seed_wheels.keys() + + +_BUNDLE_FOLDER_CACHE = {} +# defined only for self tests, which only check the current interpreter +BUNDLE_FOLDER = get_bundle_folder() +BUNDLE_SUPPORT = BundleSupport() + + def get_embed_wheel(distribution, for_py_version): - path = BUNDLE_FOLDER / (BUNDLE_SUPPORT.get(for_py_version, {}) or BUNDLE_SUPPORT[MAX]).get(distribution) + path = get_bundle_folder(for_py_version) / ( + BundleSupport(for_py_version).get(for_py_version, {}) + ).get(distribution) return Wheel.from_path(path) diff --git a/tests/integration/test_run_int.py b/tests/integration/test_run_int.py index e1dc6d3..bb459c7 100644 --- a/tests/integration/test_run_int.py +++ b/tests/integration/test_run_int.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os from typing import TYPE_CHECKING import pytest @@ -12,6 +13,9 @@ if TYPE_CHECKING: from pathlib import Path +@pytest.mark.skipif( + "NO_INTERNET" in os.environ, reason="Requires internet connection", +) @pytest.mark.skipif(IS_PYPY, reason="setuptools distutils patching does not work") def test_app_data_pinning(tmp_path: Path) -> None: version = "23.1" diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py index 5bcddf5..ed47de1 100644 --- a/tests/integration/test_zipapp.py +++ b/tests/integration/test_zipapp.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import shutil import subprocess from contextlib import suppress @@ -14,6 +15,10 @@ from virtualenv.run import cli_run HERE = Path(__file__).parent CURRENT = PythonInfo.current_system() +pytestmark = pytest.mark.skipif( + "NO_INTERNET" in os.environ, reason="Requires internet connection", +) + @pytest.fixture(scope="session") def zipapp_build_env(tmp_path_factory): 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 7db52e1..16895a5 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 @@ -25,6 +25,9 @@ if TYPE_CHECKING: @pytest.mark.slow() @pytest.mark.parametrize("copies", [False, True] if fs_supports_symlink() else [True]) +@pytest.mark.skipif( + "NO_INTERNET" in os.environ, reason="Requires internet connection" +) def test_seed_link_via_app_data(tmp_path, coverage_env, current_fastest, copies): current = PythonInfo.current_system() bundle_ver = BUNDLE_SUPPORT[current.version_release_str] diff --git a/tests/unit/seed/wheels/test_acquire_find_wheel.py b/tests/unit/seed/wheels/test_acquire_find_wheel.py index 7822849..a13466e 100644 --- a/tests/unit/seed/wheels/test_acquire_find_wheel.py +++ b/tests/unit/seed/wheels/test_acquire_find_wheel.py @@ -27,3 +27,23 @@ def test_find_exact(for_py_version): def test_find_bad_spec(): with pytest.raises(ValueError, match="bad"): find_compatible_in_house("setuptools", "bad", MAX, BUNDLE_FOLDER) + + +# ALT tests +def test_find_python2(): + """Python2 is not supported""" + with pytest.raises(FileNotFoundError): + get_embed_wheel("setuptools", "2.7") + + +def test_find_python4(): + """Future Python4 is not yet supported""" + with pytest.raises(FileNotFoundError): + get_embed_wheel("setuptools", "4.0") + + +def test_wrong_version(): + """Python version format""" + with pytest.raises(ValueError) as e: + get_embed_wheel("setuptools", "123") + assert "Unsupported value for for_py_version:" in str(e.value)