diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index dd747198fcf..6cada5be038 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -7,6 +7,7 @@ Iterable, Iterator, List, + Mapping, Optional, Sequence, Set, @@ -104,6 +105,9 @@ def __init__( self._installed_candidate_cache = ( {} ) # type: Dict[str, AlreadyInstalledCandidate] + self._extras_candidate_cache = ( + {} + ) # type: Dict[Tuple[int, FrozenSet[str]], ExtrasCandidate] if not ignore_installed: self._installed_dists = { @@ -118,6 +122,16 @@ def force_reinstall(self): # type: () -> bool return self._force_reinstall + def _make_extras_candidate(self, base, extras): + # type: (BaseCandidate, FrozenSet[str]) -> ExtrasCandidate + cache_key = (id(base), extras) + try: + candidate = self._extras_candidate_cache[cache_key] + except KeyError: + candidate = ExtrasCandidate(base, extras) + self._extras_candidate_cache[cache_key] = candidate + return candidate + def _make_candidate_from_dist( self, dist, # type: Distribution @@ -130,9 +144,9 @@ def _make_candidate_from_dist( except KeyError: base = AlreadyInstalledCandidate(dist, template, factory=self) self._installed_candidate_cache[dist.key] = base - if extras: - return ExtrasCandidate(base, extras) - return base + if not extras: + return base + return self._make_extras_candidate(base, extras) def _make_candidate_from_link( self, @@ -182,18 +196,18 @@ def _make_candidate_from_link( return None base = self._link_candidate_cache[link] - if extras: - return ExtrasCandidate(base, extras) - return base + if not extras: + return base + return self._make_extras_candidate(base, extras) def _iter_found_candidates( self, - ireqs, # type: Sequence[InstallRequirement] - specifier, # type: SpecifierSet - hashes, # type: Hashes - prefers_installed, # type: bool - ): - # type: (...) -> Iterable[Candidate] + ireqs: Sequence[InstallRequirement], + specifier: SpecifierSet, + hashes: Hashes, + prefers_installed: bool, + incompatible_ids: Set[int], + ) -> Iterable[Candidate]: if not ireqs: return () @@ -257,20 +271,27 @@ def iter_index_candidate_infos(): iter_index_candidate_infos, installed_candidate, prefers_installed, + incompatible_ids, ) def find_candidates( self, - requirements, # type: Sequence[Requirement] - constraint, # type: Constraint - prefers_installed, # type: bool - ): - # type: (...) -> Iterable[Candidate] + identifier: str, + requirements: Mapping[str, Iterator[Requirement]], + incompatibilities: Mapping[str, Iterator[Candidate]], + constraint: Constraint, + prefers_installed: bool, + ) -> Iterable[Candidate]: + + # Since we cache all the candidates, incompatibility identification + # can be made quicker by comparing only the id() values. + incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())} + explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] - for req in requirements: + for req in requirements[identifier]: cand, ireq = req.get_candidate_lookup() - if cand is not None: + if cand is not None and id(cand) not in incompat_ids: explicit_candidates.add(cand) if ireq is not None: ireqs.append(ireq) @@ -283,13 +304,14 @@ def find_candidates( constraint.specifier, constraint.hashes, prefers_installed, + incompat_ids, ) return ( c for c in explicit_candidates if constraint.is_satisfied_by(c) - and all(req.is_satisfied_by(c) for req in requirements) + and all(req.is_satisfied_by(c) for req in requirements[identifier]) ) def make_requirement_from_install_req(self, ireq, requested_extras): diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index e8b72e66000..21fa08ec938 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -100,13 +100,15 @@ class FoundCandidates(collections_abc.Sequence): def __init__( self, - get_infos, # type: Callable[[], Iterator[IndexCandidateInfo]] - installed, # type: Optional[Candidate] - prefers_installed, # type: bool + get_infos: Callable[[], Iterator[IndexCandidateInfo]], + installed: Optional[Candidate], + prefers_installed: bool, + incompatible_ids: Set[int], ): self._get_infos = get_infos self._installed = installed self._prefers_installed = prefers_installed + self._incompatible_ids = incompatible_ids def __getitem__(self, index): # type: (int) -> Candidate @@ -119,10 +121,12 @@ def __iter__(self): # type: () -> Iterator[Candidate] infos = self._get_infos() if not self._installed: - return _iter_built(infos) - if self._prefers_installed: - return _iter_built_with_prepended(self._installed, infos) - return _iter_built_with_inserted(self._installed, infos) + iterator = _iter_built(infos) + elif self._prefers_installed: + iterator = _iter_built_with_prepended(self._installed, infos) + else: + iterator = _iter_built_with_inserted(self._installed, infos) + return (c for c in iterator if id(c) not in self._incompatible_ids) def __len__(self): # type: () -> int diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 89b68489249..32597f7e093 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -149,11 +149,6 @@ def find_matches( requirements: Mapping[str, Iterator[Requirement]], incompatibilities: Mapping[str, Iterator[Candidate]], ) -> Iterable[Candidate]: - try: - current_requirements = requirements[identifier] - except KeyError: - return [] - def _eligible_for_upgrade(name): # type: (str) -> bool """Are upgrades allowed for this project? @@ -173,9 +168,11 @@ def _eligible_for_upgrade(name): return False return self._factory.find_candidates( - list(current_requirements), + identifier=identifier, + requirements=requirements, constraint=self._constraints.get(identifier, Constraint.empty()), prefers_installed=(not _eligible_for_upgrade(identifier)), + incompatibilities=incompatibilities, ) def is_satisfied_by(self, requirement, candidate): diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 6149fd1aece..1f7b0c53d17 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -59,7 +59,11 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) matches = factory.find_candidates( - [req], Constraint.empty(), prefers_installed=False, + req.name, + {req.name: [req]}, + {}, + Constraint.empty(), + prefers_installed=False, ) assert sum(1 for _ in matches) == match_count @@ -70,7 +74,11 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) candidates = factory.find_candidates( - [req], Constraint.empty(), prefers_installed=False, + req.name, + {req.name: [req]}, + {}, + Constraint.empty(), + prefers_installed=False, ) for c in candidates: assert isinstance(c, Candidate)