Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dup requirement extra merging during PEX boot. #2707

Merged
merged 1 commit into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Release Notes

## 2.33.2

This release fixes PEXes build with root requirements like `foo[bar] foo[baz]` (vs. `foo[bar,baz]`,
which worked already).

* Fix dup requirement extra merging during PEX boot. (#2707)

## 2.33.1

This release fixes a bug in both `pex3 lock subset` and
Expand Down
19 changes: 18 additions & 1 deletion pex/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ class _QualifiedRequirement(object):
requirement = attr.ib() # type: Requirement
required = attr.ib(default=True) # type: bool

def with_extras(self, extras):
# type: (FrozenSet[str]) -> _QualifiedRequirement
return attr.evolve(self, requirement=attr.evolve(self.requirement, extras=extras))


@attr.s(frozen=True)
class _DistributionNotFound(object):
Expand Down Expand Up @@ -568,7 +572,20 @@ def _root_requirements_iter(
),
V=9,
)
yield qualified_requirement

# We may have had multiple requirements that select the winning candidate distribution.
# For example, say we're a Python 3.10 interpreter and the root requirements are
# `"foo[bar]; python_version < '3.11'" "foo[baz]==1.2.3"`. In that case, we want to
# ensure that for whichever ~random requirement we selected as a representative we
# gather all extras across the candidate requirements to make sure all requested extras
# are grafted in to the resolve.
yield qualified_requirement.with_extras(
frozenset(
itertools.chain.from_iterable(
candidate[1].requirement.extras for candidate in candidates
)
)
)

def resolve(self):
# type: () -> Iterable[Distribution]
Expand Down
2 changes: 1 addition & 1 deletion pex/version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2015 Pex project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

__version__ = "2.33.1"
__version__ = "2.33.2"
72 changes: 72 additions & 0 deletions tests/integration/test_issue_2706.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2025 Pex project contributors.
# Licensed under the Apache License, Version 2.0 (see LICENSE).

from __future__ import absolute_import

import shutil
import subprocess

from pex.cache.dirs import CacheDir
from pex.common import safe_mkdir
from pex.compatibility import commonpath
from pex.pip.version import PipVersion
from pex.venv.virtualenv import InstallationChoice, Virtualenv
from testing import built_wheel, run_pex_command
from testing.pytest_utils.tmp import Tempdir


def test_extras_from_dup_root_reqs(tmpdir):
# type: (Tempdir) -> None

find_links = tmpdir.join("find-links")
safe_mkdir(find_links)

if PipVersion.DEFAULT is not PipVersion.VENDORED:
Virtualenv.create(
tmpdir.join("pip-resolver-venv"), install_pip=InstallationChoice.YES
).interpreter.execute(
args=["-m", "pip", "wheel", "--wheel-dir", find_links]
+ list(map(str, PipVersion.DEFAULT.requirements))
)

with built_wheel(
name="foo", extras_require={"bar": ["bar"], "baz": ["baz"]}
) as foo, built_wheel(name="bar") as bar, built_wheel(name="baz") as baz:
shutil.copy(foo, find_links)
shutil.copy(bar, find_links)
shutil.copy(baz, find_links)

pex_root = tmpdir.join("pex_root")
pex = tmpdir.join("pex")
run_pex_command(
args=[
"--pex-root",
pex_root,
"--runtime-pex-root",
pex_root,
"--no-pypi",
"--find-links",
find_links,
"--resolver-version",
"pip-2020-resolver",
"foo[bar]",
"foo[baz]",
"-o",
pex,
]
).assert_success()

installed_wheel_dir = CacheDir.INSTALLED_WHEELS.path(pex_root=pex_root)
for module in "foo", "bar", "baz":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B.: The import baz fails when the yield qualified_requirement.with_extras(...) in the fix above is reverted to just be the old yield qualified_requirement.

assert installed_wheel_dir == commonpath(
(
installed_wheel_dir,
subprocess.check_output(
args=[
pex,
"-c",
"import {module}; print({module}.__file__)".format(module=module),
]
).decode("utf-8"),
)
)
Loading