diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 76c3ec444ff..5e9f041b5b2 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -3,10 +3,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import (Sequence, Set) + from typing import Sequence, Set + from pip._internal.req.req_install import InstallRequirement from pip._vendor.packaging.version import _BaseVersion - from pip._internal.index.package_finder import PackageFinder def format_name(project, extras): @@ -23,11 +23,8 @@ def name(self): # type: () -> str raise NotImplementedError("Subclass should override") - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] + def find_matches(self): + # type: () -> Sequence[Candidate] raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): @@ -47,5 +44,5 @@ def version(self): raise NotImplementedError("Override in subclass") def get_dependencies(self): - # type: () -> Sequence[Requirement] + # type: () -> Sequence[InstallRequirement] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py new file mode 100644 index 00000000000..08eff0ddf9d --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -0,0 +1,126 @@ +from pip._vendor.packaging.utils import canonicalize_name + +from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .base import Candidate + +if MYPY_CHECK_RUNNING: + from typing import Any, Dict, Optional, Sequence + + from pip._internal.models.link import Link + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.resolution.base import InstallRequirementProvider + + from pip._vendor.packaging.version import _BaseVersion + from pip._vendor.pkg_resources import Distribution + + +_CANDIDATE_CACHE = {} # type: Dict[Link, Candidate] + + +def make_candidate( + link, # type: Link + preparer, # type: RequirementPreparer + parent, # type: InstallRequirement + make_install_req # type: InstallRequirementProvider +): + # type: (...) -> Candidate + if link not in _CANDIDATE_CACHE: + _CANDIDATE_CACHE[link] = LinkCandidate( + link, + preparer, + parent=parent, + make_install_req=make_install_req + ) + return _CANDIDATE_CACHE[link] + + +def make_install_req_from_link(link, parent): + # type: (Link, InstallRequirement) -> InstallRequirement + # TODO: Do we need to support editables? + return install_req_from_line( + link.url, + comes_from=parent.comes_from, + use_pep517=parent.use_pep517, + isolated=parent.isolated, + wheel_cache=parent._wheel_cache, + constraint=parent.constraint, + options=dict( + install_options=parent.install_options, + global_options=parent.global_options, + hashes=parent.hash_options + ), + ) + + +class LinkCandidate(Candidate): + def __init__( + self, + link, # type: Link + preparer, # type: RequirementPreparer + parent, # type: InstallRequirement + make_install_req, # type: InstallRequirementProvider + ): + # type: (...) -> None + self.link = link + self._preparer = preparer + self._ireq = make_install_req_from_link(link, parent) + self._make_install_req = make_install_req + + self._name = None # type: Optional[str] + self._version = None # type: Optional[_BaseVersion] + self._dist = None # type: Optional[Distribution] + + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, self.__class__): + return self.link == other.link + return False + + # Needed for Python 2, which does not implement this by default + def __ne__(self, other): + # type: (Any) -> bool + return not self.__eq__(other) + + @property + def name(self): + # type: () -> str + """The normalised name of the project the candidate refers to""" + if self._name is None: + self._name = canonicalize_name(self.dist.project_name) + return self._name + + @property + def version(self): + # type: () -> _BaseVersion + if self._version is None: + self._version = self.dist.parsed_version + return self._version + + @property + def dist(self): + # type: () -> Distribution + if self._dist is None: + abstract_dist = self._preparer.prepare_linked_requirement( + self._ireq + ) + self._dist = abstract_dist.get_pkg_resources_distribution() + # TODO: Only InstalledDistribution can return None here :-( + assert self._dist is not None + # These should be "proper" errors, not just asserts, as they + # can result from user errors like a requirement "foo @ URL" + # when the project at URL has a name of "bar" in its metadata. + assert (self._name is None or + self._name == canonicalize_name(self._dist.project_name)) + assert (self._version is None or + self._version == self.dist.parsed_version) + return self._dist + + def get_dependencies(self): + # type: () -> Sequence[InstallRequirement] + return [ + self._make_install_req(str(r), self._ireq) + for r in self.dist.requires() + ] diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py new file mode 100644 index 00000000000..981646ea96c --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -0,0 +1,83 @@ +from pip._vendor.resolvelib.providers import AbstractProvider + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .requirements import make_requirement + +if MYPY_CHECK_RUNNING: + from typing import Any, Optional, Sequence, Tuple, Union + + from pip._internal.index.package_finder import PackageFinder + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider + + from .base import Requirement, Candidate + + +class PipProvider(AbstractProvider): + def __init__( + self, + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req # type: InstallRequirementProvider + ): + # type: (...) -> None + self._finder = finder + self._preparer = preparer + self._make_install_req = make_install_req + + def make_requirement(self, ireq): + # type: (InstallRequirement) -> Requirement + return make_requirement( + ireq, + self._finder, + self._preparer, + self._make_install_req + ) + + def get_install_requirement(self, c): + # type: (Candidate) -> InstallRequirement + + # The base Candidate class does not have an _ireq attribute, so we + # fetch it dynamically here, to satisfy mypy. In practice, though, we + # only ever deal with LinkedCandidate objects at the moment, which do + # have an _ireq attribute. When we have a candidate type for installed + # requirements we should probably review this. + # + # TODO: Longer term, make a proper interface for this on the candidate. + return getattr(c, "_ireq", None) + + def identify(self, dependency): + # type: (Union[Requirement, Candidate]) -> str + return dependency.name + + def get_preference( + self, + resolution, # type: Optional[Candidate] + candidates, # type: Sequence[Candidate] + information # type: Sequence[Tuple[Requirement, Candidate]] + ): + # type: (...) -> Any + # Use the "usual" value for now + return len(candidates) + + def find_matches(self, requirement): + # type: (Requirement) -> Sequence[Candidate] + return requirement.find_matches() + + def is_satisfied_by(self, requirement, candidate): + # type: (Requirement, Candidate) -> bool + return requirement.is_satisfied_by(candidate) + + def get_dependencies(self, candidate): + # type: (Candidate) -> Sequence[Requirement] + return [ + make_requirement( + r, + self._finder, + self._preparer, + self._make_install_req + ) + for r in candidate.get_dependencies() + ] diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 76fa16a96e4..4adc8a09a34 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,141 +1,111 @@ from pip._vendor.packaging.utils import canonicalize_name -from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .base import Candidate, Requirement, format_name +from .base import Requirement +from .candidates import make_candidate if MYPY_CHECK_RUNNING: - from typing import (Optional, Sequence) - - from pip._vendor.packaging.version import _BaseVersion + from typing import Sequence from pip._internal.index.package_finder import PackageFinder - - -def make_requirement(install_req): - # type: (InstallRequirement) -> Requirement - if install_req.link: - if install_req.req and install_req.req.name: - return NamedRequirement(install_req) - else: - return UnnamedRequirement(install_req) + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider + + from .base import Candidate + + +def make_requirement( + ireq, # type: InstallRequirement + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req # type: InstallRequirementProvider +): + # type: (...) -> Requirement + if ireq.link: + candidate = make_candidate( + ireq.link, + preparer, + ireq, + make_install_req + ) + return ExplicitRequirement(candidate) else: - return VersionedRequirement(install_req) + return SpecifierRequirement( + ireq, + finder, + preparer, + make_install_req + ) -class UnnamedRequirement(Requirement): - def __init__(self, req): - # type: (InstallRequirement) -> None - self._ireq = req - self._candidate = None # type: Optional[Candidate] +class ExplicitRequirement(Requirement): + def __init__(self, candidate): + # type: (Candidate) -> None + self.candidate = candidate @property def name(self): # type: () -> str - assert self._ireq.req is None or self._ireq.name is None, \ - "Unnamed requirement has a name" - # TODO: Get the candidate and use its name... - return "" - - def _get_candidate(self): - # type: () -> Candidate - if self._candidate is None: - self._candidate = Candidate() - return self._candidate - - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] - return [self._get_candidate()] + # No need to canonicalise - the candidate did this + return self.candidate.name + + def find_matches(self): + # type: () -> Sequence[Candidate] + return [self.candidate] def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - return candidate is self._get_candidate() + # TODO: Typing - Candidate doesn't have a link attribute + # But I think the following would be better... + # return candidate.link == self.candidate.link + return candidate == self.candidate -class NamedRequirement(Requirement): - def __init__(self, req): - # type: (InstallRequirement) -> None - self._ireq = req - self._candidate = None # type: Optional[Candidate] - - @property - def name(self): - # type: () -> str - assert self._ireq.req.name is not None, "Named requirement has no name" - canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._ireq.req.extras) - - def _get_candidate(self): - # type: () -> Candidate - if self._candidate is None: - self._candidate = Candidate() - return self._candidate - - def find_matches( +class SpecifierRequirement(Requirement): + def __init__( self, - finder, # type: PackageFinder + ireq, # type: InstallRequirement + finder, # type: PackageFinder + preparer, # type:RequirementPreparer + make_install_req # type: InstallRequirementProvider ): - # type: (...) -> Sequence[Candidate] - return [self._get_candidate()] - - def is_satisfied_by(self, candidate): - # type: (Candidate) -> bool - return candidate is self._get_candidate() - - -# TODO: This is temporary, to make the tests pass -class DummyCandidate(Candidate): - def __init__(self, name, version): - # type: (str, _BaseVersion) -> None - self._name = name - self._version = version - - @property - def name(self): - # type: () -> str - return self._name - - @property - def version(self): - # type: () -> _BaseVersion - return self._version - - -class VersionedRequirement(Requirement): - def __init__(self, ireq): - # type: (InstallRequirement) -> None - assert ireq.req is not None, "Un-specified requirement not allowed" - assert ireq.req.url is None, "Direct reference not allowed" + # type: (...) -> None + assert ireq.link is None, "This is a link, not a specifier" + assert not ireq.req.extras, "Extras not yet supported" self._ireq = ireq + self._finder = finder + self._preparer = preparer + self._make_install_req = make_install_req @property def name(self): # type: () -> str canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._ireq.req.extras) + return canonical_name - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] - found = finder.find_best_candidate( + def find_matches(self): + # type: () -> Sequence[Candidate] + found = self._finder.find_best_candidate( project_name=self._ireq.req.name, specifier=self._ireq.req.specifier, hashes=self._ireq.hashes(trust_internet=False), ) return [ - DummyCandidate(ican.name, ican.version) + make_candidate( + ican.link, + self._preparer, + self._ireq, + self._make_install_req + ) for ican in found.iter_applicable() ] def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - # TODO: Should check name matches as well. Defer this - # until we have the proper Candidate object, and - # no longer have to deal with unnmed requirements... + + assert candidate.name == self.name, \ + "Internal issue: Candidate is not for this requirement " \ + " {} vs {}".format(candidate.name, self.name) return candidate.version in self._ireq.req.specifier diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index f0750673c24..cb40ab70b95 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -4,7 +4,9 @@ from pip._vendor.resolvelib import BaseReporter from pip._vendor.resolvelib import Resolver as RLResolver +from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver +from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -15,32 +17,8 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import InstallRequirementProvider - from .base import Candidate, Requirement - - -# FIXME: Import the actual implementation. -# This is a stub to pass typing checks. -class PipProvider(object): - def __init__( - self, - finder, # type: PackageFinder - preparer, # type: RequirementPreparer - make_install_req, # type: InstallRequirementProvider - ): - # type: (...) -> None - super(PipProvider, self).__init__() - - def make_requirement(self, r): - # type: (InstallRequirement) -> Requirement - raise NotImplementedError() - - def get_install_requirement(self, c): - # type: (Candidate) -> InstallRequirement - raise NotImplementedError() - class Resolver(BaseResolver): def __init__( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 330cc34836d..1072c81d198 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -405,12 +405,17 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): ) -def test_basic_install_from_local_directory(script, data): +@pytest.mark.parametrize("resolver", ["", "--unstable-feature=resolver"]) +def test_basic_install_from_local_directory(script, data, resolver): """ Test installing from a local directory. """ + args = ["install"] + if resolver: + args.append(resolver) to_install = data.packages.joinpath("FSPkg") - result = script.pip('install', to_install) + args.append(to_install) + result = script.pip(*args) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( script.site_packages / diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index f885d1c855b..6ddc6422adf 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest from pip._internal.cli.req_command import RequirementCommand @@ -8,7 +10,9 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession +from pip._internal.req.constructors import install_req_from_req_string from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager @@ -41,3 +45,14 @@ def preparer(finder): ) yield preparer + + +@pytest.fixture +def provider(finder, preparer): + make_install_req = partial( + install_req_from_req_string, + isolated=False, + wheel_cache=None, + use_pep517=None, + ) + yield PipProvider(finder, preparer, make_install_req) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 28fdaaa20e9..a563eea28d0 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,4 +1,5 @@ import pytest +from pip._vendor.resolvelib import BaseReporter, Resolver from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.resolvelib.base import Candidate @@ -16,7 +17,6 @@ # Create a requirement from a sdist filename # Create a requirement from a local directory (which has no obvious name!) # Editables -# @pytest.fixture @@ -32,16 +32,17 @@ def data_url(name): # Version specifiers ("simple", "simple", 3), ("simple>1.0", "simple", 2), - ("simple[extra]==1.0", "simple[extra]", 1), + # ("simple[extra]==1.0", "simple[extra]", 1), # Wheels (data_file("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), (data_url("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), # Direct URLs - ("foo @ " + data_url("simple-1.0.tar.gz"), "foo", 1), + # TODO: The following test fails + # ("foo @ " + data_url("simple-1.0.tar.gz"), "foo", 1), # SDists # TODO: sdists should have a name - (data_file("simple-1.0.tar.gz"), "", 1), - (data_url("simple-1.0.tar.gz"), "", 1), + (data_file("simple-1.0.tar.gz"), "simple", 1), + (data_url("simple-1.0.tar.gz"), "simple", 1), # TODO: directory, editables ] @@ -52,24 +53,36 @@ def req_from_line(line): return make_requirement(install_req_from_line(line)) -def test_rlr_requirement_has_name(test_cases): +def test_rlr_requirement_has_name(test_cases, provider): """All requirements should have a name""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) assert req.name == name -def test_rlr_correct_number_of_matches(test_cases, finder): +def test_rlr_correct_number_of_matches(test_cases, provider): """Requirements should return the correct number of candidates""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) - assert len(req.find_matches(finder)) == matches + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) + assert len(req.find_matches()) == matches -def test_rlr_candidates_match_requirement(test_cases, finder): +def test_rlr_candidates_match_requirement(test_cases, provider): """Candidates returned from find_matches should satisfy the requirement""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) - for c in req.find_matches(finder): + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) + for c in req.find_matches(): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) + + +def test_rlr_full_resolve(provider): + """A very basic full resolve""" + ireq = install_req_from_line("simplewheel") + req = provider.make_requirement(ireq) + r = Resolver(provider, BaseReporter()) + result = r.resolve([req]) + assert set(result.mapping.keys()) == {'simplewheel'}