3636from pip ._internal .models .link import Link
3737from pip ._internal .models .wheel import Wheel
3838from 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+ )
4043from 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 ]
0 commit comments