Skip to content

Commit 9fd8c80

Browse files
authored
Fix dup requirement extra merging during PEX boot. (#2707)
Previously, using root requirements of `foo[bar] foo[baz]` would produce the same PEX as `foo[bar,baz]`, (save for the root requirements list) but the former PEX would not properly activate both the `bar` and `baz` extras during boot. This is now fixed. Fixes #2706.
1 parent 5880b9b commit 9fd8c80

File tree

4 files changed

+98
-2
lines changed

4 files changed

+98
-2
lines changed

CHANGES.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Release Notes
22

3+
## 2.33.2
4+
5+
This release fixes PEXes build with root requirements like `foo[bar] foo[baz]` (vs. `foo[bar,baz]`,
6+
which worked already).
7+
8+
* Fix dup requirement extra merging during PEX boot. (#2707)
9+
310
## 2.33.1
411

512
This release fixes a bug in both `pex3 lock subset` and

pex/environment.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ class _QualifiedRequirement(object):
177177
requirement = attr.ib() # type: Requirement
178178
required = attr.ib(default=True) # type: bool
179179

180+
def with_extras(self, extras):
181+
# type: (FrozenSet[str]) -> _QualifiedRequirement
182+
return attr.evolve(self, requirement=attr.evolve(self.requirement, extras=extras))
183+
180184

181185
@attr.s(frozen=True)
182186
class _DistributionNotFound(object):
@@ -568,7 +572,20 @@ def _root_requirements_iter(
568572
),
569573
V=9,
570574
)
571-
yield qualified_requirement
575+
576+
# We may have had multiple requirements that select the winning candidate distribution.
577+
# For example, say we're a Python 3.10 interpreter and the root requirements are
578+
# `"foo[bar]; python_version < '3.11'" "foo[baz]==1.2.3"`. In that case, we want to
579+
# ensure that for whichever ~random requirement we selected as a representative we
580+
# gather all extras across the candidate requirements to make sure all requested extras
581+
# are grafted in to the resolve.
582+
yield qualified_requirement.with_extras(
583+
frozenset(
584+
itertools.chain.from_iterable(
585+
candidate[1].requirement.extras for candidate in candidates
586+
)
587+
)
588+
)
572589

573590
def resolve(self):
574591
# type: () -> Iterable[Distribution]

pex/version.py

+1-1
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.33.1"
4+
__version__ = "2.33.2"

tests/integration/test_issue_2706.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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 shutil
7+
import subprocess
8+
9+
from pex.cache.dirs import CacheDir
10+
from pex.common import safe_mkdir
11+
from pex.compatibility import commonpath
12+
from pex.pip.version import PipVersion
13+
from pex.venv.virtualenv import InstallationChoice, Virtualenv
14+
from testing import built_wheel, run_pex_command
15+
from testing.pytest_utils.tmp import Tempdir
16+
17+
18+
def test_extras_from_dup_root_reqs(tmpdir):
19+
# type: (Tempdir) -> None
20+
21+
find_links = tmpdir.join("find-links")
22+
safe_mkdir(find_links)
23+
24+
if PipVersion.DEFAULT is not PipVersion.VENDORED:
25+
Virtualenv.create(
26+
tmpdir.join("pip-resolver-venv"), install_pip=InstallationChoice.YES
27+
).interpreter.execute(
28+
args=["-m", "pip", "wheel", "--wheel-dir", find_links]
29+
+ list(map(str, PipVersion.DEFAULT.requirements))
30+
)
31+
32+
with built_wheel(
33+
name="foo", extras_require={"bar": ["bar"], "baz": ["baz"]}
34+
) as foo, built_wheel(name="bar") as bar, built_wheel(name="baz") as baz:
35+
shutil.copy(foo, find_links)
36+
shutil.copy(bar, find_links)
37+
shutil.copy(baz, find_links)
38+
39+
pex_root = tmpdir.join("pex_root")
40+
pex = tmpdir.join("pex")
41+
run_pex_command(
42+
args=[
43+
"--pex-root",
44+
pex_root,
45+
"--runtime-pex-root",
46+
pex_root,
47+
"--no-pypi",
48+
"--find-links",
49+
find_links,
50+
"--resolver-version",
51+
"pip-2020-resolver",
52+
"foo[bar]",
53+
"foo[baz]",
54+
"-o",
55+
pex,
56+
]
57+
).assert_success()
58+
59+
installed_wheel_dir = CacheDir.INSTALLED_WHEELS.path(pex_root=pex_root)
60+
for module in "foo", "bar", "baz":
61+
assert installed_wheel_dir == commonpath(
62+
(
63+
installed_wheel_dir,
64+
subprocess.check_output(
65+
args=[
66+
pex,
67+
"-c",
68+
"import {module}; print({module}.__file__)".format(module=module),
69+
]
70+
).decode("utf-8"),
71+
)
72+
)

0 commit comments

Comments
 (0)