diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6ed1814da..fd32f196c 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -32,6 +32,7 @@ jobs: - pypy-3.10 - pypy-3.9 - pypy-3.8 + - graalpy-24.1 os: - ubuntu-latest - macos-latest @@ -41,6 +42,7 @@ jobs: - { os: macos-latest, py: "brew@3.10" } - { os: macos-latest, py: "brew@3.9" } exclude: + - { os: windows-latest, py: "graalpy-24.1" } - { os: windows-latest, py: "pypy-3.10" } - { os: windows-latest, py: "pypy-3.9" } - { os: windows-latest, py: "pypy-3.8" } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45cdc68e1..080e6af6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows args: ["--verbose"] @@ -24,7 +24,7 @@ repos: hooks: - id: pyproject-fmt - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.9.9" + rev: "v0.11.2" hooks: - id: ruff-format - id: ruff diff --git a/docs/changelog.rst b/docs/changelog.rst index 502236053..902709bc5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,19 @@ Release History .. towncrier release notes start +v20.30.0 (2025-03-31) +--------------------- + +Features - 20.30.0 +~~~~~~~~~~~~~~~~~~ +- Add support for `GraalPy `_. (:issue:`2832`) + +Bugfixes - 20.30.0 +~~~~~~~~~~~~~~~~~~ +- Upgrade embedded wheels: + + * setuptools to ``78.1.0`` from ``75.3.2`` (:issue:`2863`) + v20.29.3 (2025-03-06) --------------------- diff --git a/pyproject.toml b/pyproject.toml index 851b90bc8..0cf61b8a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,7 @@ optional-dependencies.test = [ "packaging>=23.1", "pytest>=7.4", "pytest-env>=0.8.2", - "pytest-freezer>=0.4.8; platform_python_implementation=='PyPy' or (platform_python_implementation=='CPython' and sys_platform=='win32' and python_version>='3.13')", + "pytest-freezer>=0.4.8; platform_python_implementation=='PyPy' or platform_python_implementation=='GraalVM' or (platform_python_implementation=='CPython' and sys_platform=='win32' and python_version>='3.13')", "pytest-mock>=3.11.1", "pytest-randomly>=3.12", "pytest-timeout>=2.1", @@ -87,6 +87,8 @@ entry-points."virtualenv.create".cpython3-mac-brew = "virtualenv.create.via_glob entry-points."virtualenv.create".cpython3-mac-framework = "virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsFramework" entry-points."virtualenv.create".cpython3-posix = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Posix" entry-points."virtualenv.create".cpython3-win = "virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows" +entry-points."virtualenv.create".graalpy-posix = "virtualenv.create.via_global_ref.builtin.graalpy:GraalPyPosix" +entry-points."virtualenv.create".graalpy-win = "virtualenv.create.via_global_ref.builtin.graalpy:GraalPyWindows" entry-points."virtualenv.create".pypy3-posix = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:PyPy3Posix" entry-points."virtualenv.create".pypy3-win = "virtualenv.create.via_global_ref.builtin.pypy.pypy3:Pypy3Windows" entry-points."virtualenv.create".venv = "virtualenv.create.via_global_ref.venv:Venv" diff --git a/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py b/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py new file mode 100644 index 000000000..8bfe887e7 --- /dev/null +++ b/src/virtualenv/create/via_global_ref/builtin/graalpy/__init__.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from abc import ABC +from pathlib import Path + +from virtualenv.create.describe import PosixSupports, WindowsSupports +from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest, RefMust, RefWhen +from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGlobalRefVirtualenvBuiltin + + +class GraalPy(ViaGlobalRefVirtualenvBuiltin, ABC): + @classmethod + def can_describe(cls, interpreter): + return interpreter.implementation == "GraalVM" and super().can_describe(interpreter) + + @classmethod + def exe_stem(cls): + return "graalpy" + + @classmethod + def exe_names(cls, interpreter): + return { + cls.exe_stem(), + "python", + f"python{interpreter.version_info.major}", + f"python{interpreter.version_info.major}.{interpreter.version_info.minor}", + } + + @classmethod + def _executables(cls, interpreter): + host = Path(interpreter.system_executable) + targets = sorted(f"{name}{cls.suffix}" for name in cls.exe_names(interpreter)) + yield host, targets, RefMust.NA, RefWhen.ANY + + @classmethod + def sources(cls, interpreter): + yield from super().sources(interpreter) + python_dir = Path(interpreter.system_executable).resolve().parent + if python_dir.name in {"bin", "Scripts"}: + python_dir = python_dir.parent + + native_lib = cls._native_lib(python_dir / "lib", interpreter.platform) + if native_lib.exists(): + yield PathRefToDest(native_lib, dest=lambda self, s: self.bin_dir.parent / "lib" / s.name) + + for jvm_dir_name in ("jvm", "jvmlibs", "modules"): + jvm_dir = python_dir / jvm_dir_name + if jvm_dir.exists(): + yield PathRefToDest(jvm_dir, dest=lambda self, s: self.bin_dir.parent / s.name) + + @classmethod + def _shared_libs(cls, python_dir): + raise NotImplementedError + + def set_pyenv_cfg(self): + super().set_pyenv_cfg() + # GraalPy 24.0 and older had home without the bin + version = self.interpreter.version_info + if version.major == 3 and version.minor <= 10: # noqa: PLR2004 + home = Path(self.pyenv_cfg["home"]) + if home.name == "bin": + self.pyenv_cfg["home"] = str(home.parent) + + +class GraalPyPosix(GraalPy, PosixSupports): + @classmethod + def _native_lib(cls, lib_dir, platform): + if platform == "darwin": + return lib_dir / "libpythonvm.dylib" + return lib_dir / "libpythonvm.so" + + +class GraalPyWindows(GraalPy, WindowsSupports): + @classmethod + def _native_lib(cls, lib_dir, _platform): + return lib_dir / "pythonvm.dll" + + def set_pyenv_cfg(self): + # GraalPy needs an additional entry in pyvenv.cfg on Windows + super().set_pyenv_cfg() + self.pyenv_cfg["venvlauncher_command"] = self.interpreter.system_executable + + +__all__ = [ + "GraalPyPosix", + "GraalPyWindows", +] diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py index be978116e..9f16e37a7 100644 --- a/src/virtualenv/discovery/py_info.py +++ b/src/virtualenv/discovery/py_info.py @@ -11,6 +11,7 @@ import os import platform import re +import struct import sys import sysconfig import warnings @@ -44,7 +45,10 @@ def abs_path(v): # this is a tuple in earlier, struct later, unify to our own named tuple self.version_info = VersionInfo(*sys.version_info) - self.architecture = 64 if sys.maxsize > 2**32 else 32 + # Use the same implementation as found in stdlib platform.architecture + # to account for platforms where the maximum integer is not equal the + # pointer size. + self.architecture = 32 if struct.calcsize("P") == 4 else 64 # noqa: PLR2004 # Used to determine some file names. # See `CPython3Windows.python_zip()`. diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py index 6f8c2bdf3..51c84343c 100644 --- a/src/virtualenv/info.py +++ b/src/virtualenv/info.py @@ -8,6 +8,7 @@ IMPLEMENTATION = platform.python_implementation() IS_PYPY = IMPLEMENTATION == "PyPy" +IS_GRAALPY = IMPLEMENTATION == "GraalVM" IS_CPYTHON = IMPLEMENTATION == "CPython" IS_WIN = sys.platform == "win32" IS_MAC_ARM64 = sys.platform == "darwin" and platform.machine() == "arm64" @@ -55,6 +56,7 @@ def fs_path_id(path: str) -> str: __all__ = ( "IS_CPYTHON", + "IS_GRAALPY", "IS_MAC_ARM64", "IS_PYPY", "IS_WIN", diff --git a/src/virtualenv/seed/wheels/embed/__init__.py b/src/virtualenv/seed/wheels/embed/__init__.py index cf60a041f..4727e6101 100644 --- a/src/virtualenv/seed/wheels/embed/__init__.py +++ b/src/virtualenv/seed/wheels/embed/__init__.py @@ -8,37 +8,37 @@ BUNDLE_SUPPORT = { "3.8": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.3.0-py3-none-any.whl", + "setuptools": "setuptools-75.3.2-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.9": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.10": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.11": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.12": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.13": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, "3.14": { "pip": "pip-25.0.1-py3-none-any.whl", - "setuptools": "setuptools-75.8.0-py3-none-any.whl", + "setuptools": "setuptools-78.1.0-py3-none-any.whl", "wheel": "wheel-0.45.1-py3-none-any.whl", }, } diff --git a/src/virtualenv/seed/wheels/embed/setuptools-75.3.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-75.3.2-py3-none-any.whl similarity index 88% rename from src/virtualenv/seed/wheels/embed/setuptools-75.3.0-py3-none-any.whl rename to src/virtualenv/seed/wheels/embed/setuptools-75.3.2-py3-none-any.whl index b6a97ce19..1b66a67ff 100644 Binary files a/src/virtualenv/seed/wheels/embed/setuptools-75.3.0-py3-none-any.whl and b/src/virtualenv/seed/wheels/embed/setuptools-75.3.2-py3-none-any.whl differ diff --git a/src/virtualenv/seed/wheels/embed/setuptools-75.8.0-py3-none-any.whl b/src/virtualenv/seed/wheels/embed/setuptools-78.1.0-py3-none-any.whl similarity index 63% rename from src/virtualenv/seed/wheels/embed/setuptools-75.8.0-py3-none-any.whl rename to src/virtualenv/seed/wheels/embed/setuptools-78.1.0-py3-none-any.whl index 65ee9ceb4..a1f43daab 100644 Binary files a/src/virtualenv/seed/wheels/embed/setuptools-75.8.0-py3-none-any.whl and b/src/virtualenv/seed/wheels/embed/setuptools-78.1.0-py3-none-any.whl differ diff --git a/tasks/pick_tox_env.py b/tasks/pick_tox_env.py index 2b0bffdbe..ff37b14d5 100644 --- a/tasks/pick_tox_env.py +++ b/tasks/pick_tox_env.py @@ -7,6 +7,8 @@ py = sys.argv[1] if py.startswith("brew@"): py = py[len("brew@") :] +if py.startswith("graalpy-"): + py = "graalpy" env = f"TOXENV={py}" if len(sys.argv) > 2: # noqa: PLR2004 env += f"\nTOX_BASEPYTHON={sys.argv[2]}" diff --git a/tests/conftest.py b/tests/conftest.py index 0310b5525..e4fc28479 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,12 +13,13 @@ from virtualenv.app_data import AppDataDiskFolder from virtualenv.discovery.py_info import PythonInfo -from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink +from virtualenv.info import IS_GRAALPY, IS_PYPY, IS_WIN, fs_supports_symlink from virtualenv.report import LOGGER def pytest_addoption(parser): parser.addoption("--int", action="store_true", default=False, help="run integration tests") + parser.addoption("--skip-slow", action="store_true", default=False, help="skip slow tests") def pytest_configure(config): @@ -46,6 +47,11 @@ def pytest_collection_modifyitems(config, items): if item.location[0].startswith(int_location): item.add_marker(pytest.mark.skip(reason="need --int option to run")) + if config.getoption("--skip-slow"): + for item in items: + if "slow" in [mark.name for mark in item.iter_markers()]: + item.add_marker(pytest.mark.skip(reason="skipped because --skip-slow was passed")) + @pytest.fixture(scope="session") def has_symlink_support(tmp_path_factory): # noqa: ARG001 @@ -355,7 +361,7 @@ def _skip_if_test_in_system(session_app_data): pytest.skip("test not valid if run under system") -if IS_PYPY or (IS_WIN and sys.version_info[0:2] >= (3, 13)): # https://github.com/adamchainz/time-machine/issues/456 +if IS_PYPY or IS_GRAALPY: @pytest.fixture def time_freeze(freezer): diff --git a/tests/integration/test_zipapp.py b/tests/integration/test_zipapp.py index e666657a9..3e7a0707d 100644 --- a/tests/integration/test_zipapp.py +++ b/tests/integration/test_zipapp.py @@ -19,7 +19,7 @@ @pytest.fixture(scope="session") def zipapp_build_env(tmp_path_factory): create_env_path = None - if CURRENT.implementation != "PyPy": + if CURRENT.implementation not in {"PyPy", "GraalVM"}: exe = CURRENT.executable # guaranteed to contain a recent enough pip (tox.ini) else: create_env_path = tmp_path_factory.mktemp("zipapp-create-env") @@ -112,6 +112,7 @@ def test_zipapp_help(call_zipapp, capsys): assert not err +@pytest.mark.slow @pytest.mark.parametrize("seeder", ["app-data", "pip"]) def test_zipapp_create(call_zipapp, seeder): call_zipapp("--seeder", seeder) diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index 2e4078d1c..ed1cb11a2 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -398,6 +398,7 @@ def test_create_long_path(tmp_path): subprocess.check_call([str(result.creator.script("pip")), "--version"]) +@pytest.mark.slow @pytest.mark.parametrize("creator", sorted(set(PythonInfo.current_system().creators().key_to_class) - {"builtin"})) @pytest.mark.usefixtures("session_app_data") def test_create_distutils_cfg(creator, tmp_path, monkeypatch): diff --git a/tests/unit/create/via_global_ref/test_build_c_ext.py b/tests/unit/create/via_global_ref/test_build_c_ext.py index 08320673a..5195ff856 100644 --- a/tests/unit/create/via_global_ref/test_build_c_ext.py +++ b/tests/unit/create/via_global_ref/test_build_c_ext.py @@ -26,6 +26,7 @@ def builtin_shows_marker_missing(): return not marker.exists() +@pytest.mark.slow @pytest.mark.xfail( condition=bool(os.environ.get("CI_RUN")), strict=False, diff --git a/tests/unit/discovery/py_info/test_py_info.py b/tests/unit/discovery/py_info/test_py_info.py index c15387726..43452b014 100644 --- a/tests/unit/discovery/py_info/test_py_info.py +++ b/tests/unit/discovery/py_info/test_py_info.py @@ -416,11 +416,11 @@ def test_fallback_existent_system_executable(mocker): mocker.patch.object(sys, "executable", current.executable) # ensure it falls back to an alternate binary name that exists - current._fast_get_system_executable() # noqa: SLF001 - assert os.path.basename(current.system_executable) in [ + system_executable = current._fast_get_system_executable() # noqa: SLF001 + assert os.path.basename(system_executable) in [ f"python{v}" for v in (current.version_info.major, f"{current.version_info.major}.{current.version_info.minor}") ] - assert os.path.exists(current.system_executable) + assert os.path.exists(system_executable) @pytest.mark.skipif(sys.version_info[:2] != (3, 10), reason="3.10 specific") 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 4fd3d30c9..2448d908a 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 @@ -147,6 +147,7 @@ def read_only_app_data(temp_app_data): yield temp_app_data +@pytest.mark.slow @pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") @pytest.mark.usefixtures("read_only_app_data") def test_base_bootstrap_link_via_app_data_not_writable(tmp_path, current_fastest): @@ -155,6 +156,7 @@ def test_base_bootstrap_link_via_app_data_not_writable(tmp_path, current_fastest assert result +@pytest.mark.slow @pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") def test_populated_read_only_cache_and_symlinked_app_data(tmp_path, current_fastest, temp_app_data): dest = tmp_path / "venv" @@ -180,6 +182,7 @@ def test_populated_read_only_cache_and_symlinked_app_data(tmp_path, current_fast check_call((str(dest.joinpath("bin/python")), "-c", "import pip")) +@pytest.mark.slow @pytest.mark.skipif(sys.platform == "win32", reason="Windows only applies R/O to files") def test_populated_read_only_cache_and_copied_app_data(tmp_path, current_fastest, temp_app_data): dest = tmp_path / "venv" diff --git a/tests/unit/seed/wheels/test_periodic_update.py b/tests/unit/seed/wheels/test_periodic_update.py index 3172bc8e6..3b0529a39 100644 --- a/tests/unit/seed/wheels/test_periodic_update.py +++ b/tests/unit/seed/wheels/test_periodic_update.py @@ -280,6 +280,7 @@ def test_trigger_update_no_debug(for_py_version, session_app_data, tmp_path, moc monkeypatch.delenv("_VIRTUALENV_PERIODIC_UPDATE_INLINE", raising=False) current = get_embed_wheel("setuptools", for_py_version) process = mocker.MagicMock() + process.pid = 123 process.communicate.return_value = None, None Popen = mocker.patch("virtualenv.seed.wheels.periodic_update.Popen", return_value=process) # noqa: N806 @@ -328,6 +329,7 @@ def test_trigger_update_debug(for_py_version, session_app_data, tmp_path, mocker current = get_embed_wheel("pip", for_py_version) process = mocker.MagicMock() + process.pid = 123 process.communicate.return_value = None, None Popen = mocker.patch("virtualenv.seed.wheels.periodic_update.Popen", return_value=process) # noqa: N806 diff --git a/tox.ini b/tox.ini index 3feaa4ef0..c805ab43f 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ env_list = 3.10 3.9 3.8 + graalpy coverage readme docs @@ -32,12 +33,13 @@ set_env = PYTHONWARNDEFAULTENCODING = 1 _COVERAGE_SRC = {envsitepackagesdir}/virtualenv commands = - coverage erase - coverage run -m pytest {posargs:--junitxml "{toxworkdir}/junit.{envname}.xml" tests --int} - coverage combine - coverage report --skip-covered --show-missing - coverage xml -o "{toxworkdir}/coverage.{envname}.xml" - coverage html -d {envtmpdir}/htmlcov --show-contexts --title virtualenv-{envname}-coverage + !graalpy: coverage erase + !graalpy: coverage run -m pytest {posargs:--junitxml "{toxworkdir}/junit.{envname}.xml" tests --int} + !graalpy: coverage combine + !graalpy: coverage report --skip-covered --show-missing + !graalpy: coverage xml -o "{toxworkdir}/coverage.{envname}.xml" + !graalpy: coverage html -d {envtmpdir}/htmlcov --show-contexts --title virtualenv-{envname}-coverage + graalpy: pytest {posargs:--junitxml "{toxworkdir}/junit.{envname}.xml" tests --skip-slow} uv_seed = true [testenv:fix]