1+ import contextlib
12import functools
23import logging
34from typing import (
1617 cast ,
1718)
1819
20+ from pip ._vendor .packaging .requirements import InvalidRequirement
21+ from pip ._vendor .packaging .requirements import Requirement as PackagingRequirement
1922from pip ._vendor .packaging .specifiers import SpecifierSet
2023from pip ._vendor .packaging .utils import NormalizedName , canonicalize_name
2124from pip ._vendor .pkg_resources import Distribution
5457 ExtrasCandidate ,
5558 LinkCandidate ,
5659 RequiresPythonCandidate ,
60+ as_base_candidate ,
5761)
5862from .found_candidates import FoundCandidates , IndexCandidateInfo
5963from .requirements import (
@@ -123,6 +127,15 @@ def force_reinstall(self):
123127 # type: () -> bool
124128 return self ._force_reinstall
125129
130+ def _fail_if_link_is_unsupported_wheel (self , link : Link ) -> None :
131+ if not link .is_wheel :
132+ return
133+ wheel = Wheel (link .filename )
134+ if wheel .supported (self ._finder .target_python .get_tags ()):
135+ return
136+ msg = f"{ link .filename } is not a supported wheel on this platform."
137+ raise UnsupportedWheel (msg )
138+
126139 def _make_extras_candidate (self , base , extras ):
127140 # type: (BaseCandidate, FrozenSet[str]) -> ExtrasCandidate
128141 cache_key = (id (base ), extras )
@@ -275,6 +288,51 @@ def iter_index_candidate_infos():
275288 incompatible_ids ,
276289 )
277290
291+ def _iter_explicit_candidates_from_base (
292+ self ,
293+ base_requirements : Iterable [Requirement ],
294+ extras : FrozenSet [str ],
295+ ) -> Iterator [Candidate ]:
296+ """Produce explicit candidates from the base given an extra-ed package.
297+
298+ :param base_requirements: Requirements known to the resolver. The
299+ requirements are guaranteed to not have extras.
300+ :param extras: The extras to inject into the explicit requirements'
301+ candidates.
302+ """
303+ for req in base_requirements :
304+ lookup_cand , _ = req .get_candidate_lookup ()
305+ if lookup_cand is None : # Not explicit.
306+ continue
307+ # We've stripped extras from the identifier, and should always
308+ # get a BaseCandidate here, unless there's a bug elsewhere.
309+ base_cand = as_base_candidate (lookup_cand )
310+ assert base_cand is not None , "no extras here"
311+ yield self ._make_extras_candidate (base_cand , extras )
312+
313+ def _iter_candidates_from_constraints (
314+ self ,
315+ identifier : str ,
316+ constraint : Constraint ,
317+ template : InstallRequirement ,
318+ ) -> Iterator [Candidate ]:
319+ """Produce explicit candidates from constraints.
320+
321+ This creates "fake" InstallRequirement objects that are basically clones
322+ of what "should" be the template, but with original_link set to link.
323+ """
324+ for link in constraint .links :
325+ self ._fail_if_link_is_unsupported_wheel (link )
326+ candidate = self ._make_candidate_from_link (
327+ link ,
328+ extras = frozenset (),
329+ template = install_req_from_link_and_ireq (link , template ),
330+ name = canonicalize_name (identifier ),
331+ version = None ,
332+ )
333+ if candidate :
334+ yield candidate
335+
278336 def find_candidates (
279337 self ,
280338 identifier : str ,
@@ -283,59 +341,48 @@ def find_candidates(
283341 constraint : Constraint ,
284342 prefers_installed : bool ,
285343 ) -> Iterable [Candidate ]:
286-
287- # Since we cache all the candidates, incompatibility identification
288- # can be made quicker by comparing only the id() values.
289- incompat_ids = {id (c ) for c in incompatibilities .get (identifier , ())}
290-
344+ # Collect basic lookup information from the requirements.
291345 explicit_candidates = set () # type: Set[Candidate]
292346 ireqs = [] # type: List[InstallRequirement]
293347 for req in requirements [identifier ]:
294348 cand , ireq = req .get_candidate_lookup ()
295- if cand is not None and id ( cand ) not in incompat_ids :
349+ if cand is not None :
296350 explicit_candidates .add (cand )
297351 if ireq is not None :
298352 ireqs .append (ireq )
299353
300- for link in constraint .links :
301- if not ireqs :
302- # If we hit this condition, then we cannot construct a candidate.
303- # However, if we hit this condition, then none of the requirements
304- # provided an ireq, so they must have provided an explicit candidate.
305- # In that case, either the candidate matches, in which case this loop
306- # doesn't need to do anything, or it doesn't, in which case there's
307- # nothing this loop can do to recover.
308- break
309- if link .is_wheel :
310- wheel = Wheel (link .filename )
311- # Check whether the provided wheel is compatible with the target
312- # platform.
313- if not wheel .supported (self ._finder .target_python .get_tags ()):
314- # We are constrained to install a wheel that is incompatible with
315- # the target architecture, so there are no valid candidates.
316- # Return early, with no candidates.
317- return ()
318- # Create a "fake" InstallRequirement that's basically a clone of
319- # what "should" be the template, but with original_link set to link.
320- # Using the given requirement is necessary for preserving hash
321- # requirements, but without the original_link, direct_url.json
322- # won't be created.
323- ireq = install_req_from_link_and_ireq (link , ireqs [0 ])
324- candidate = self ._make_candidate_from_link (
325- link ,
326- extras = frozenset (),
327- template = ireq ,
328- name = canonicalize_name (ireq .name ) if ireq .name else None ,
329- version = None ,
354+ # If the current identifier contains extras, add explicit candidates
355+ # from entries from extra-less identifier.
356+ with contextlib .suppress (InvalidRequirement ):
357+ parsed_requirement = PackagingRequirement (identifier )
358+ explicit_candidates .update (
359+ self ._iter_explicit_candidates_from_base (
360+ requirements .get (parsed_requirement .name , ()),
361+ frozenset (parsed_requirement .extras ),
362+ ),
330363 )
331- if candidate is None :
332- # _make_candidate_from_link returns None if the wheel fails to build.
333- # We are constrained to install this wheel, so there are no valid
334- # candidates.
335- # Return early, with no candidates.
364+
365+ # Add explicit candidates from constraints. We only do this if there are
366+ # kown ireqs, which represent requirements not already explicit. If
367+ # there are no ireqs, we're constraining already-explicit requirements,
368+ # which is handled later when we return the explicit candidates.
369+ if ireqs :
370+ try :
371+ explicit_candidates .update (
372+ self ._iter_candidates_from_constraints (
373+ identifier ,
374+ constraint ,
375+ template = ireqs [0 ],
376+ ),
377+ )
378+ except UnsupportedWheel :
379+ # If we're constrained to install a wheel incompatible with the
380+ # target architecture, no candidates will ever be valid.
336381 return ()
337382
338- explicit_candidates .add (candidate )
383+ # Since we cache all the candidates, incompatibility identification
384+ # can be made quicker by comparing only the id() values.
385+ incompat_ids = {id (c ) for c in incompatibilities .get (identifier , ())}
339386
340387 # If none of the requirements want an explicit candidate, we can ask
341388 # the finder for candidates.
@@ -351,7 +398,8 @@ def find_candidates(
351398 return (
352399 c
353400 for c in explicit_candidates
354- if constraint .is_satisfied_by (c )
401+ if id (c ) not in incompat_ids
402+ and constraint .is_satisfied_by (c )
355403 and all (req .is_satisfied_by (c ) for req in requirements [identifier ])
356404 )
357405
@@ -366,13 +414,7 @@ def make_requirement_from_install_req(self, ireq, requested_extras):
366414 return None
367415 if not ireq .link :
368416 return SpecifierRequirement (ireq )
369- if ireq .link .is_wheel :
370- wheel = Wheel (ireq .link .filename )
371- if not wheel .supported (self ._finder .target_python .get_tags ()):
372- msg = "{} is not a supported wheel on this platform." .format (
373- wheel .filename ,
374- )
375- raise UnsupportedWheel (msg )
417+ self ._fail_if_link_is_unsupported_wheel (ireq .link )
376418 cand = self ._make_candidate_from_link (
377419 ireq .link ,
378420 extras = frozenset (ireq .extras ),
0 commit comments