diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c849752a..eea98d86d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,8 +76,8 @@ jobs: - py311-pip20 - py311-pip22_3_1 - py311-pip23_1_2 - - py312-pip24_1_2 - - py313-pip24_1_2 + - py312-pip24_2 + - py313-pip24_2 - pypy310-pip20 - pypy310-pip22_3_1 - pypy310-pip23_1_2 @@ -87,8 +87,8 @@ jobs: - py311-pip20-integration - py311-pip22_3_1-integration - py311-pip23_1_2-integration - - py312-pip24_1_2-integration - - py313-pip24_1_2-integration + - py312-pip24_2-integration + - py313-pip24_2-integration - pypy310-pip20-integration - pypy310-pip22_3_1-integration - pypy310-pip23_1_2-integration @@ -132,10 +132,10 @@ jobs: matrix: include: - python-version: [ 3, 12 ] - tox-env: py312-pip24_1_2 + tox-env: py312-pip24_2 tox-env-python: python3.11 - python-version: [ 3, 12 ] - tox-env: py312-pip24_1_2-integration + tox-env: py312-pip24_2-integration tox-env-python: python3.11 steps: - name: Calculate Pythons to Expose diff --git a/pex/pep_440.py b/pex/pep_440.py index 86550e314..b745a0a98 100644 --- a/pex/pep_440.py +++ b/pex/pep_440.py @@ -73,6 +73,12 @@ def __lt__(self, other): return NotImplemented return self.parsed_version < other.parsed_version + def __ge__(self, other): + # type: (Any) -> bool + if not isinstance(other, Version): + return NotImplemented + return self.parsed_version >= other.parsed_version + @property def is_legacy(self): # type: () -> bool diff --git a/pex/pip/version.py b/pex/pip/version.py index ba8fe3f91..aeaa96465 100644 --- a/pex/pip/version.py +++ b/pex/pip/version.py @@ -16,11 +16,16 @@ from pex.typing import TYPE_CHECKING, cast if TYPE_CHECKING: - from typing import Iterable, Optional, Tuple, Union + from typing import Iterable, Iterator, Optional, Tuple, Union @functools.total_ordering class PipVersionValue(Enum.Value): + @classmethod + def _iter_values(cls): + # type: () -> Iterator[PipVersionValue] + return cast("Iterator[PipVersionValue]", super(PipVersionValue, cls)._iter_values()) + @classmethod def overridden(cls): # type: () -> Optional[PipVersionValue] @@ -94,9 +99,15 @@ def __lt__(self, other): return NotImplemented return self.version < other.version + def __ge__(self, other): + if not isinstance(other, PipVersionValue): + return NotImplemented + return self.version >= other.version + class LatestPipVersion(object): def __get__(self, obj, objtype=None): + # type: (...) -> PipVersionValue if not hasattr(self, "_latest"): self._latest = max( (version for version in PipVersionValue._iter_values() if not version.hidden), @@ -111,17 +122,16 @@ def __init__(self, preferred): self._preferred = preferred def __get__(self, obj, objtype=None): + # type: (...) -> PipVersionValue if not hasattr(self, "_default"): - self._default = None current_version = Version(".".join(map(str, sys.version_info[:3]))) - preferred_versions = ( - [PipVersionValue.overridden()] if PipVersionValue.overridden() else self._preferred - ) + overridden = PipVersionValue.overridden() + preferred_versions = [overridden] if overridden is not None else self._preferred for preferred_version in preferred_versions: if preferred_version.requires_python_applies(current_version): self._default = preferred_version break - if self._default is None: + if not hasattr(self, "_default"): applicable_versions = tuple( version for version in PipVersionValue._iter_values() @@ -285,6 +295,13 @@ def values(cls): requires_python=">=3.8,<3.14", ) + v24_2 = PipVersionValue( + version="24.2", + setuptools_version="71.1.0", + wheel_version="0.43.0", + requires_python=">=3.8,<3.14", + ) + VENDORED = v20_3_4_patched LATEST = LatestPipVersion() DEFAULT = DefaultPipVersion(preferred=(VENDORED, v23_2, v24_1)) diff --git a/tests/integration/test_excludes.py b/tests/integration/test_excludes.py index b15a72bb7..057920f9f 100644 --- a/tests/integration/test_excludes.py +++ b/tests/integration/test_excludes.py @@ -52,12 +52,11 @@ def assert_certifi_is_excluded(pex): ) -@pytest.fixture(scope="module") -def requests_certifi_excluded_pex(tmpdir_factory): +def requests_certifi_excluded_pex(tmpdir): # type: (Any) -> str - pex_root = str(tmpdir_factory.mktemp("pex_root")) - pex = str(tmpdir_factory.mktemp("pex")) + pex_root = os.path.join(str(tmpdir), "pex_root") + pex = os.path.join(str(tmpdir), "pex") run_pex_command( args=[ "--lock", @@ -147,28 +146,28 @@ def assert_requests_certifi_excluded_pex( @skip_unless_compatible_with_requests_lock def test_exclude( tmpdir, # type: Any - requests_certifi_excluded_pex, # type: str certifi_venv, # type: Virtualenv ): # type: (...) -> None - assert_requests_certifi_excluded_pex(requests_certifi_excluded_pex, certifi_venv) + pex = requests_certifi_excluded_pex(tmpdir) + assert_requests_certifi_excluded_pex(pex, certifi_venv) @skip_unless_compatible_with_requests_lock def test_requirements_pex_exclude( tmpdir, # type: Any - requests_certifi_excluded_pex, # type: str certifi_venv, # type: Virtualenv ): # type: (...) -> None - pex_root = PexInfo.from_pex(requests_certifi_excluded_pex).pex_root + requirements_pex = requests_certifi_excluded_pex(tmpdir) + pex_root = PexInfo.from_pex(requirements_pex).pex_root pex = os.path.join(str(tmpdir), "pex") run_pex_command( args=[ "--requirements-pex", - requests_certifi_excluded_pex, + requirements_pex, "ansicolors==1.1.8", "-o", pex, diff --git a/tests/integration/test_issue_2017.py b/tests/integration/test_issue_2017.py index 468d4f61c..db9b4fce0 100644 --- a/tests/integration/test_issue_2017.py +++ b/tests/integration/test_issue_2017.py @@ -11,54 +11,83 @@ import pytest +from pex.atomic_directory import atomic_directory from pex.common import is_exe, safe_open from pex.compatibility import urlparse from pex.fetcher import URLFetcher -from pex.pep_440 import Version -from pex.pip.version import PipVersion +from pex.pip.version import PipVersion, PipVersionValue from pex.typing import TYPE_CHECKING from testing import IS_LINUX, run_pex_command if TYPE_CHECKING: - from typing import Any + from typing import Any, Iterator + + +def musl_libc_capable_pip_versions(): + # type: () -> Iterator[PipVersionValue] + + for version in PipVersion.values(): + if not version.requires_python_applies(): + continue + if version is PipVersion.VENDORED or version >= PipVersion.v24_2: + yield version + + +MUSL_LIBC_CAPABLE_PIP_VERSIONS = tuple(musl_libc_capable_pip_versions()) + + +@pytest.fixture +def statically_linked_musl_libc_cpython(shared_integration_test_tmpdir): + # type: (str) -> str + pbs_distribution_url = ( + "https://github.com/indygreg/python-build-standalone/releases/download/20221220/" + "cpython-3.10.9+20221220-x86_64_v3-unknown-linux-musl-install_only.tar.gz" + ) + tarball_name = os.path.basename(urlparse.urlparse(pbs_distribution_url).path) + pbs_distribution = os.path.join(shared_integration_test_tmpdir, "PBS-dists", tarball_name) + with atomic_directory(pbs_distribution) as chroot: + if not chroot.is_finalized(): + tarball_dest = os.path.join(chroot.work_dir, tarball_name) + with URLFetcher().get_body_stream(pbs_distribution_url) as read_fp, open( + tarball_dest, "wb" + ) as write_fp: + shutil.copyfileobj(read_fp, write_fp) + with tarfile.open(tarball_dest) as tf: + tf.extractall(chroot.work_dir) + statically_linked_musl_libc_cpython = os.path.join( + chroot.work_dir, "python", "bin", "python3" + ) + assert is_exe(statically_linked_musl_libc_cpython) + + return os.path.join(pbs_distribution, "python", "bin", "python3") -# TODO(John Sirois): Include a test of >= Pip 24.2 when Pex adds support for it. -# See: https://github.com/pex-tool/pex/issues/2471 @pytest.mark.skipif( - PipVersion.DEFAULT > PipVersion.VENDORED and PipVersion.DEFAULT.version < Version("24.2"), + not MUSL_LIBC_CAPABLE_PIP_VERSIONS, reason=( "Although Pex's vendored Pip is patched to handle statically linked musl libc CPython, no " - "version of Pip Pex supports handles these Pythons until Pip 24.2" + "version of Pip Pex supports handles these Pythons until Pip 24.2 and none of these " + "versions are supported by the current interpreter." ), ) @pytest.mark.skipif( not IS_LINUX, reason="This test tests statically linked musl libc CPython which is only available for Linux.", ) -def test_statically_linked_musl_libc_cpython_support(tmpdir): - # type: (Any) -> None - - pbs_distribution_url = ( - "https://github.com/indygreg/python-build-standalone/releases/download/20221220/" - "cpython-3.10.9+20221220-x86_64_v3-unknown-linux-musl-install_only.tar.gz" - ) - pbs_distribution = os.path.join( - str(tmpdir), - os.path.basename(urlparse.urlparse(pbs_distribution_url).path), - ) - with URLFetcher().get_body_stream(pbs_distribution_url) as read_fp, open( - pbs_distribution, "wb" - ) as write_fp: - shutil.copyfileobj(read_fp, write_fp) - with tarfile.open(pbs_distribution) as tf: - tf.extractall(str(tmpdir)) - statically_linked_musl_libc_cpython = os.path.join(str(tmpdir), "python", "bin", "python3") - assert is_exe(statically_linked_musl_libc_cpython) +@pytest.mark.parametrize( + "pip_version", + [pytest.param(version, id=str(version)) for version in MUSL_LIBC_CAPABLE_PIP_VERSIONS], +) +def test_statically_linked_musl_libc_cpython_support( + tmpdir, # type: Any + pip_version, # type: PipVersionValue + statically_linked_musl_libc_cpython, # type: str +): + # type: (...) -> None pex = os.path.join(str(tmpdir), "pex") run_pex_command( - args=["fortune==1.1.1", "-c", "fortune", "-o", pex], + args=["fortune==1.1.1", "-c", "fortune", "--pip-version", str(pip_version), "-o", pex], python=statically_linked_musl_libc_cpython, ).assert_success() diff --git a/tox.ini b/tox.ini index b318170ae..751fdd2ab 100644 --- a/tox.ini +++ b/tox.ini @@ -90,6 +90,7 @@ setenv = pip24_1: _PEX_PIP_VERSION=24.1 pip24_1_1: _PEX_PIP_VERSION=24.1.1 pip24_1_2: _PEX_PIP_VERSION=24.1.2 + pip24_2: _PEX_PIP_VERSION=24.2 # Python 3 (until a fix here in 3.9: https://bugs.python.org/issue13601) switched from stderr # being unbuffered to stderr being buffered by default. This can lead to tests checking stderr