Skip to content

Commit 659e813

Browse files
authored
Support adhoc Pip versions in development. (#3011)
Enables #2177.
1 parent af5be46 commit 659e813

File tree

7 files changed

+174
-26
lines changed

7 files changed

+174
-26
lines changed

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Release Notes
22

3+
## 2.70.0
4+
5+
This release adds a feature for Pex developers. If you want to experiment with a new version of Pip
6+
you can now specify `_PEX_PIP_VERSION=adhoc _PEX_PIP_ADHOC_REQUIREMENT=...`. N.B.: This feature is
7+
for Pex development only.
8+
9+
* Support adhoc Pip versions in development. (#3011)
10+
311
## 2.69.2
412

513
This release fixes handling of scoped repos. Previously, validation against duplicate scopes was too

pex/cache/dirs.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,10 @@ def iter_all(cls, pex_root=ENV):
614614
from pex.pip.version import PipVersion
615615

616616
for base_dir in glob.glob(CacheDir.PIP.path("*", pex_root=pex_root)):
617-
version = PipVersion.for_value(os.path.basename(base_dir))
617+
dir_name = os.path.basename(base_dir)
618+
version = (
619+
PipVersion.ADHOC if dir_name.startswith("adhoc") else PipVersion.for_value(dir_name)
620+
)
618621
cache_dir = os.path.join(base_dir, "pip_cache")
619622
for pex_dir in glob.glob(os.path.join(base_dir, "pip.pex", "*", "*")):
620623
yield cls(path=pex_dir, version=version, base_dir=base_dir, cache_dir=cache_dir)
@@ -629,7 +632,7 @@ def create(
629632

630633
from pex.third_party import isolated
631634

632-
base_dir = CacheDir.PIP.path(str(version))
635+
base_dir = CacheDir.PIP.path(version.cache_dir_name())
633636
return cls(
634637
path=os.path.join(base_dir, "pip.pex", isolated().pex_hash, fingerprint),
635638
version=version,

pex/enum.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,13 @@ def __reduce__(self):
7272
)
7373
return _get_or_create, (module, enum_type, type(self).__name__, self.value)
7474

75-
def __init__(self, value):
76-
# type: (str) -> None
77-
values = Enum.Value._values_by_type[type(self)]
75+
def __init__(
76+
self,
77+
value, # type: str
78+
enum_type=None, # type: Optional[Type[Enum.Value]]
79+
):
80+
# type: (...) -> None
81+
values = Enum.Value._values_by_type[enum_type or type(self)]
7882
self.value = value
7983
self.ordinal = len(values)
8084
values.append(weakref.ref(self))

pex/pip/version.py

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from __future__ import absolute_import
55

66
import functools
7+
import hashlib
78
import os
89
import sys
910
from textwrap import dedent
@@ -42,6 +43,22 @@ def overridden(cls):
4243
break
4344
return cast("Optional[PipVersionValue]", getattr(cls, "_overridden"))
4445

46+
@staticmethod
47+
def _to_requirement(
48+
project_name, # type: str
49+
project_version, # type: Union[str, Version]
50+
):
51+
# type: (...) -> Requirement
52+
53+
if not project_version:
54+
return Requirement.parse(project_name)
55+
56+
return Requirement.parse(
57+
"{project_name}=={project_version}".format(
58+
project_name=project_name, project_version=project_version
59+
)
60+
)
61+
4562
def __init__(
4663
self,
4764
version, # type: str
@@ -54,34 +71,34 @@ def __init__(
5471
hidden=False, # type: bool
5572
):
5673
# type: (...) -> None
57-
super(PipVersionValue, self).__init__(name or version)
58-
59-
def to_requirement(
60-
project_name, # type: str
61-
project_version, # type: str
62-
):
63-
# type: (...) -> Requirement
64-
return Requirement.parse(
65-
"{project_name}=={project_version}".format(
66-
project_name=project_name, project_version=project_version
67-
)
68-
)
74+
super(PipVersionValue, self).__init__(name or version, enum_type=PipVersionValue)
6975

7076
self.version = Version(version)
71-
self.requirement = (
72-
Requirement.parse(requirement) if requirement else to_requirement("pip", version)
73-
)
77+
self._requirement = requirement
7478
self.setuptools_version = setuptools_version
7579
self.setuptools_requirement = (
7680
Requirement.parse(setuptools_requirement)
7781
if setuptools_requirement
78-
else to_requirement("setuptools", setuptools_version)
82+
else self._to_requirement("setuptools", setuptools_version)
7983
)
8084
self.wheel_version = wheel_version
81-
self.wheel_requirement = to_requirement("wheel", wheel_version)
85+
self.wheel_requirement = self._to_requirement("wheel", wheel_version)
8286
self.requires_python = SpecifierSet(requires_python) if requires_python else None
8387
self.hidden = hidden
8488

89+
def cache_dir_name(self):
90+
# type: () -> str
91+
return self.value
92+
93+
@property
94+
def requirement(self):
95+
# type: () -> Requirement
96+
return (
97+
Requirement.parse(self._requirement)
98+
if self._requirement
99+
else self._to_requirement("pip", self.version)
100+
)
101+
85102
@property
86103
def requirements(self):
87104
# type: () -> Iterable[Requirement]
@@ -124,9 +141,9 @@ def __get__(self, obj, objtype=None):
124141
class LatestCompatiblePipVersion(object):
125142
def __get__(self, obj, objtype=None):
126143
# type: (...) -> PipVersionValue
127-
if not hasattr(self, "_latest"):
128-
self._latest = PipVersion.latest_compatible()
129-
return self._latest
144+
if not hasattr(self, "_latest_compatible"):
145+
self._latest_compatible = PipVersion.latest_compatible()
146+
return self._latest_compatible
130147

131148

132149
class DefaultPipVersion(object):
@@ -171,6 +188,39 @@ def __get__(self, obj, objtype=None):
171188
return self._default
172189

173190

191+
class Adhoc(PipVersionValue):
192+
def __init__(self):
193+
super(Adhoc, self).__init__(
194+
name="adhoc",
195+
version="99",
196+
setuptools_version="",
197+
wheel_version="",
198+
requires_python="",
199+
hidden=True,
200+
)
201+
self._adhoc_requirement = None # type: Optional[Requirement]
202+
203+
def cache_dir_name(self):
204+
# type: () -> str
205+
return "adhoc-{fingerprint}".format(
206+
fingerprint=hashlib.sha1(str(self.requirement).encode("utf-8")).hexdigest()
207+
)
208+
209+
@property
210+
def requirement(self):
211+
# type: () -> Requirement
212+
213+
if self._adhoc_requirement is None:
214+
requirement = os.environ.get("_PEX_PIP_ADHOC_REQUIREMENT")
215+
if not requirement:
216+
raise ValueError(
217+
"You must set a value for the _PEX_PIP_ADHOC_REQUIREMENT environment variable "
218+
"to use an adhoc Pip version."
219+
)
220+
self._adhoc_requirement = Requirement.parse(requirement)
221+
return self._adhoc_requirement
222+
223+
174224
class PipVersion(Enum["PipVersionValue"]):
175225
@classmethod
176226
def values(cls):
@@ -376,6 +426,8 @@ def latest_compatible(cls, target=None):
376426
requires_python=">=3.9,<3.16",
377427
)
378428

429+
ADHOC = Adhoc()
430+
379431
VENDORED = v20_3_4_patched
380432
LATEST = LatestPipVersion()
381433
LATEST_COMPATIBLE = LatestCompatiblePipVersion()

pex/venv/venv_pex.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def maybe_log(*message):
172172
"_PEX_HTTP_SERVER_TIMEOUT",
173173
"_PEX_PEXPECT_TIMEOUT",
174174
"_PEX_PIP_VERSION",
175+
"_PEX_PIP_ADHOC_REQUIREMENT",
175176
"_PEX_REQUIRES_PYTHON",
176177
"_PEX_TEST_DEV_ROOT",
177178
"_PEX_TEST_POS_ARGS",

pex/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Copyright 2015 Pex project contributors.
22
# Licensed under the Apache License, Version 2.0 (see LICENSE).
33

4-
__version__ = "2.69.2"
4+
__version__ = "2.70.0"

tests/integration/test_pip.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2025 Pex project contributors.
2+
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3+
4+
from __future__ import absolute_import
5+
6+
import re
7+
import sys
8+
9+
import pytest
10+
11+
from testing import make_env, run_pex_command
12+
from testing.pytest_utils.tmp import Tempdir
13+
14+
15+
@pytest.mark.skipif(
16+
sys.version_info[:2] < (3, 9),
17+
reason="The adhoc Pip version used in the test requires Python>=3.9.",
18+
)
19+
def test_adhoc_nominal(tmpdir):
20+
# type: (Tempdir) -> None
21+
22+
pex_root = tmpdir.join("pex-root")
23+
pip_log = tmpdir.join("pip.log")
24+
run_pex_command(
25+
args=[
26+
"--pex-root",
27+
pex_root,
28+
"--runtime-pex-root",
29+
pex_root,
30+
"--pip-log",
31+
pip_log,
32+
"cowsay==5",
33+
"-c",
34+
"cowsay",
35+
"--",
36+
"Moo!",
37+
],
38+
env=make_env(
39+
_PEX_PIP_VERSION="adhoc",
40+
# N.B.: This is a custom version of Pip that prints "Pex Adhoc Proof!" to STDERR just
41+
# before running the rest of Pip.
42+
_PEX_PIP_ADHOC_REQUIREMENT="pip @ git+https://github.com/pex-tool/pip@2c03ed1a2d60b57f",
43+
),
44+
).assert_success(
45+
expected_output_re=r"^.*{message}.*$".format(message=re.escape("| Moo! |")),
46+
re_flags=re.DOTALL | re.MULTILINE,
47+
)
48+
49+
with open(pip_log) as fp:
50+
assert (
51+
"Pex Adhoc Proof!" in fp.read()
52+
), "Expected proof we were running the right adhoc Pip version."
53+
54+
55+
def test_adhoc_missing_req(tmpdir):
56+
# type: (Tempdir) -> None
57+
58+
pex_root = tmpdir.join("pex-root")
59+
run_pex_command(
60+
args=[
61+
"--pex-root",
62+
pex_root,
63+
"--runtime-pex-root",
64+
pex_root,
65+
"cowsay==5",
66+
"-c",
67+
"cowsay",
68+
"--",
69+
"Moo!",
70+
],
71+
env=make_env(_PEX_PIP_VERSION="adhoc"),
72+
).assert_failure(
73+
expected_error_re=r"^.*{message}.*$".format(
74+
message=re.escape(
75+
"You must set a value for the _PEX_PIP_ADHOC_REQUIREMENT environment variable to "
76+
"use an adhoc Pip version."
77+
)
78+
),
79+
re_flags=re.DOTALL | re.MULTILINE,
80+
)

0 commit comments

Comments
 (0)