mirror of https://github.com/pypa/pip
Implement PipProvider
This commit is contained in:
parent
9e15cd49f2
commit
7d2eb544b5
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
]
|
|
@ -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()
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue