Implement PipProvider

This commit is contained in:
Paul Moore 2020-03-12 15:18:47 +00:00
parent 9e15cd49f2
commit 7d2eb544b5
6 changed files with 284 additions and 119 deletions

View File

@ -3,10 +3,10 @@ from pip._vendor.packaging.utils import canonicalize_name
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 @@ class Requirement(object):
# 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 @@ class Candidate(object):
raise NotImplementedError("Override in subclass")
def get_dependencies(self):
# type: () -> Sequence[Requirement]
# type: () -> Sequence[InstallRequirement]
raise NotImplementedError("Override in subclass")

View File

@ -0,0 +1,112 @@
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 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
# Dummy to make lint pass
_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]
@property
def name(self):
# type: () -> str
if self._name is None:
self._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 == 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(r, self._ireq)
for r in self.dist.requires()
]

View File

@ -0,0 +1,71 @@
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 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()
]

View File

@ -1,141 +1,108 @@
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
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(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)
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 ""
return self.candidate.name
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()]
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"
return candidate.version in self._ireq.req.specifier

View File

@ -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.index.package_finder import PackageFinder
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)

View File

@ -16,7 +16,6 @@ from pip._internal.utils.urls import path_to_url
# Create a requirement from a sdist filename
# Create a requirement from a local directory (which has no obvious name!)
# Editables
#
@pytest.fixture
@ -32,16 +31,17 @@ def test_cases(data):
# 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 +52,27 @@ 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)