diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 8a96ee20f0c..23686f76ac2 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -21,7 +21,7 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Optional, Set, Tuple, TypeVar + from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -29,7 +29,6 @@ from pip._vendor.resolvelib import ResolutionImpossible from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement @@ -66,7 +65,7 @@ def __init__( if not ignore_installed: self._installed_dists = { - dist.project_name: dist + canonicalize_name(dist.project_name): dist for dist in get_installed_distributions() } else: @@ -93,6 +92,8 @@ def _make_candidate_from_link( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate + # TODO: Check already installed candidate, and use it if the link and + # editable flag match. if parent.editable: if link not in self._editable_candidate_cache: self._editable_candidate_cache[link] = EditableCandidate( @@ -109,32 +110,40 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base - def make_candidate_from_ican( - self, - ican, # type: InstallationCandidate - extras, # type: Set[str] - parent, # type: InstallRequirement - ): - # type: (...) -> Candidate - dist = self._installed_dists.get(ican.name) - should_use_installed_dist = ( - not self._force_reinstall and - dist is not None and - dist.parsed_version == ican.version + def iter_found_candidates(self, ireq, extras): + # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] + name = canonicalize_name(ireq.req.name) + if not self._force_reinstall: + installed_dist = self._installed_dists.get(name) + else: + installed_dist = None + + found = self.finder.find_best_candidate( + project_name=ireq.req.name, + specifier=ireq.req.specifier, + hashes=ireq.hashes(trust_internet=False), ) - if not should_use_installed_dist: - return self._make_candidate_from_link( + for ican in found.iter_applicable(): + if (installed_dist is not None and + installed_dist.parsed_version == ican.version): + continue + yield self._make_candidate_from_link( link=ican.link, extras=extras, - parent=parent, - name=canonicalize_name(ican.name), + parent=ireq, + name=name, version=ican.version, ) - return self._make_candidate_from_dist( - dist=dist, - extras=extras, - parent=parent, - ) + + # Return installed distribution if it matches the specifier. This is + # done last so the resolver will prefer it over downloading links. + if (installed_dist is not None and + installed_dist.parsed_version in ireq.req.specifier): + yield self._make_candidate_from_dist( + dist=installed_dist, + extras=extras, + parent=ireq, + ) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 8682b2f7ede..97a41feee25 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -69,19 +69,8 @@ def name(self): def find_matches(self): # type: () -> Sequence[Candidate] - found = self._factory.finder.find_best_candidate( - project_name=self._ireq.req.name, - specifier=self._ireq.req.specifier, - hashes=self._ireq.hashes(trust_internet=False), - ) - return [ - self._factory.make_candidate_from_ican( - ican=ican, - extras=self.extras, - parent=self._ireq, - ) - for ican in found.iter_applicable() - ] + it = self._factory.iter_found_candidates(self._ireq, self.extras) + return list(it) def is_satisfied_by(self, candidate): # type: (Candidate) -> bool diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index cb384d7a0ac..3e28843d1f3 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -89,6 +89,63 @@ def test_new_resolver_picks_latest_version(script): assert_installed(script, simple="0.2.0") +def test_new_resolver_picks_installed_version(script): + create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + create_basic_wheel_for_package( + script, + "simple", + "0.2.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple==0.1.0" + ) + assert_installed(script, simple="0.1.0") + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert "Collecting" not in result.stdout, "Should not fetch new version" + assert_installed(script, simple="0.1.0") + + +def test_new_resolver_picks_installed_version_if_no_match_found(script): + create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + create_basic_wheel_for_package( + script, + "simple", + "0.2.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple==0.1.0" + ) + assert_installed(script, simple="0.1.0") + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "simple" + ) + assert "Collecting" not in result.stdout, "Should not fetch new version" + assert_installed(script, simple="0.1.0") + + def test_new_resolver_installs_dependencies(script): create_basic_wheel_for_package( script,