Skip to content

Commit 417ca92

Browse files
authored
Merge pull request #12392 from sanderr/issue/12373-bugfix-install-local-extra
Fixed bug in extras handling for link requirements
2 parents a15dd75 + 83e41d9 commit 417ca92

File tree

4 files changed

+86
-19
lines changed

4 files changed

+86
-19
lines changed

news/12372.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug in extras handling for link requirements

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
from pip._internal.models.link import Link
3737
from pip._internal.models.wheel import Wheel
3838
from pip._internal.operations.prepare import RequirementPreparer
39-
from pip._internal.req.constructors import install_req_from_link_and_ireq
39+
from pip._internal.req.constructors import (
40+
install_req_drop_extras,
41+
install_req_from_link_and_ireq,
42+
)
4043
from pip._internal.req.req_install import (
4144
InstallRequirement,
4245
check_invalid_constraint_type,
@@ -176,6 +179,20 @@ def _make_candidate_from_link(
176179
name: Optional[NormalizedName],
177180
version: Optional[CandidateVersion],
178181
) -> Optional[Candidate]:
182+
base: Optional[BaseCandidate] = self._make_base_candidate_from_link(
183+
link, template, name, version
184+
)
185+
if not extras or base is None:
186+
return base
187+
return self._make_extras_candidate(base, extras, comes_from=template)
188+
189+
def _make_base_candidate_from_link(
190+
self,
191+
link: Link,
192+
template: InstallRequirement,
193+
name: Optional[NormalizedName],
194+
version: Optional[CandidateVersion],
195+
) -> Optional[BaseCandidate]:
179196
# TODO: Check already installed candidate, and use it if the link and
180197
# editable flag match.
181198

@@ -204,7 +221,7 @@ def _make_candidate_from_link(
204221
self._build_failures[link] = e
205222
return None
206223

207-
base: BaseCandidate = self._editable_candidate_cache[link]
224+
return self._editable_candidate_cache[link]
208225
else:
209226
if link not in self._link_candidate_cache:
210227
try:
@@ -224,11 +241,7 @@ def _make_candidate_from_link(
224241
)
225242
self._build_failures[link] = e
226243
return None
227-
base = self._link_candidate_cache[link]
228-
229-
if not extras:
230-
return base
231-
return self._make_extras_candidate(base, extras, comes_from=template)
244+
return self._link_candidate_cache[link]
232245

233246
def _iter_found_candidates(
234247
self,
@@ -362,9 +375,8 @@ def _iter_candidates_from_constraints(
362375
"""
363376
for link in constraint.links:
364377
self._fail_if_link_is_unsupported_wheel(link)
365-
candidate = self._make_candidate_from_link(
378+
candidate = self._make_base_candidate_from_link(
366379
link,
367-
extras=frozenset(),
368380
template=install_req_from_link_and_ireq(link, template),
369381
name=canonicalize_name(identifier),
370382
version=None,
@@ -454,10 +466,10 @@ def _make_requirements_from_install_req(
454466
Returns requirement objects associated with the given InstallRequirement. In
455467
most cases this will be a single object but the following special cases exist:
456468
- the InstallRequirement has markers that do not apply -> result is empty
457-
- the InstallRequirement has both a constraint and extras -> result is split
458-
in two requirement objects: one with the constraint and one with the
459-
extra. This allows centralized constraint handling for the base,
460-
resulting in fewer candidate rejections.
469+
- the InstallRequirement has both a constraint (or link) and extras
470+
-> result is split in two requirement objects: one with the constraint
471+
(or link) and one with the extra. This allows centralized constraint
472+
handling for the base, resulting in fewer candidate rejections.
461473
"""
462474
if not ireq.match_markers(requested_extras):
463475
logger.info(
@@ -471,10 +483,13 @@ def _make_requirements_from_install_req(
471483
yield SpecifierRequirement(ireq)
472484
else:
473485
self._fail_if_link_is_unsupported_wheel(ireq.link)
474-
cand = self._make_candidate_from_link(
486+
# Always make the link candidate for the base requirement to make it
487+
# available to `find_candidates` for explicit candidate lookup for any
488+
# set of extras.
489+
# The extras are required separately via a second requirement.
490+
cand = self._make_base_candidate_from_link(
475491
ireq.link,
476-
extras=frozenset(ireq.extras),
477-
template=ireq,
492+
template=install_req_drop_extras(ireq) if ireq.extras else ireq,
478493
name=canonicalize_name(ireq.name) if ireq.name else None,
479494
version=None,
480495
)
@@ -489,7 +504,13 @@ def _make_requirements_from_install_req(
489504
raise self._build_failures[ireq.link]
490505
yield UnsatisfiableRequirement(canonicalize_name(ireq.name))
491506
else:
507+
# require the base from the link
492508
yield self.make_requirement_from_candidate(cand)
509+
if ireq.extras:
510+
# require the extras on top of the base candidate
511+
yield self.make_requirement_from_candidate(
512+
self._make_extras_candidate(cand, frozenset(ireq.extras))
513+
)
493514

494515
def collect_root_requirements(
495516
self, root_ireqs: List[InstallRequirement]

tests/functional/test_install_reqs.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -671,9 +671,9 @@ def test_install_distribution_union_with_versions(
671671
expect_error=(resolver_variant == "resolvelib"),
672672
)
673673
if resolver_variant == "resolvelib":
674-
assert "Cannot install localextras[bar]" in result.stderr
675-
assert ("localextras[bar] 0.0.1 depends on localextras 0.0.1") in result.stdout
676-
assert ("localextras[baz] 0.0.2 depends on localextras 0.0.2") in result.stdout
674+
assert "Cannot install localextras" in result.stderr
675+
assert ("The user requested localextras 0.0.1") in result.stdout
676+
assert ("The user requested localextras 0.0.2") in result.stdout
677677
else:
678678
assert (
679679
"Successfully installed LocalExtras-0.0.1 simple-3.0 singlemodule-0.0.1"

tests/functional/test_new_resolver.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,51 @@ def test_new_resolver_respect_user_requested_if_extra_is_installed(
24062406
script.assert_installed(pkg3="1.0", pkg2="2.0", pkg1="1.0")
24072407

24082408

2409+
def test_new_resolver_constraint_on_link_with_extra(
2410+
script: PipTestEnvironment,
2411+
) -> None:
2412+
"""
2413+
Verify that installing works from a link with both an extra and a constraint.
2414+
"""
2415+
wheel: pathlib.Path = create_basic_wheel_for_package(
2416+
script, "pkg", "1.0", extras={"ext": []}
2417+
)
2418+
2419+
script.pip(
2420+
"install",
2421+
"--no-cache-dir",
2422+
# no index, no --find-links: only the explicit path
2423+
"--no-index",
2424+
f"{wheel}[ext]",
2425+
"pkg==1",
2426+
)
2427+
script.assert_installed(pkg="1.0")
2428+
2429+
2430+
def test_new_resolver_constraint_on_link_with_extra_indirect(
2431+
script: PipTestEnvironment,
2432+
) -> None:
2433+
"""
2434+
Verify that installing works from a link with an extra if there is an indirect
2435+
dependency on that same package with the same extra (#12372).
2436+
"""
2437+
wheel_one: pathlib.Path = create_basic_wheel_for_package(
2438+
script, "pkg1", "1.0", extras={"ext": []}
2439+
)
2440+
wheel_two: pathlib.Path = create_basic_wheel_for_package(
2441+
script, "pkg2", "1.0", depends=["pkg1[ext]==1.0"]
2442+
)
2443+
2444+
script.pip(
2445+
"install",
2446+
"--no-cache-dir",
2447+
# no index, no --find-links: only the explicit path
2448+
wheel_two,
2449+
f"{wheel_one}[ext]",
2450+
)
2451+
script.assert_installed(pkg1="1.0", pkg2="1.0")
2452+
2453+
24092454
def test_new_resolver_do_not_backtrack_on_build_failure(
24102455
script: PipTestEnvironment,
24112456
) -> None:

0 commit comments

Comments
 (0)