mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Implement the resolvelib Requirement class
This commit is contained in:
parent
f981facb70
commit
9b10b93503
0
news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial
Normal file
0
news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial
Normal file
51
src/pip/_internal/resolution/resolvelib/base.py
Normal file
51
src/pip/_internal/resolution/resolvelib/base.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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 pip._vendor.packaging.version import _BaseVersion
|
||||||
|
from pip._internal.index.package_finder import PackageFinder
|
||||||
|
|
||||||
|
|
||||||
|
def format_name(project, extras):
|
||||||
|
# type: (str, Set[str]) -> str
|
||||||
|
if not extras:
|
||||||
|
return project
|
||||||
|
canonical_extras = sorted(canonicalize_name(e) for e in extras)
|
||||||
|
return "{}[{}]".format(project, ",".join(canonical_extras))
|
||||||
|
|
||||||
|
|
||||||
|
class Requirement(object):
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# type: () -> str
|
||||||
|
raise NotImplementedError("Subclass should override")
|
||||||
|
|
||||||
|
def find_matches(
|
||||||
|
self,
|
||||||
|
finder, # type: PackageFinder
|
||||||
|
):
|
||||||
|
# type: (...) -> Sequence[Candidate]
|
||||||
|
raise NotImplementedError("Subclass should override")
|
||||||
|
|
||||||
|
def is_satisfied_by(self, candidate):
|
||||||
|
# type: (Candidate) -> bool
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class Candidate(object):
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# type: () -> str
|
||||||
|
raise NotImplementedError("Override in subclass")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
# type: () -> _BaseVersion
|
||||||
|
raise NotImplementedError("Override in subclass")
|
||||||
|
|
||||||
|
def get_dependencies(self):
|
||||||
|
# type: () -> Sequence[Requirement]
|
||||||
|
raise NotImplementedError("Override in subclass")
|
141
src/pip/_internal/resolution/resolvelib/requirements.py
Normal file
141
src/pip/_internal/resolution/resolvelib/requirements.py
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
if MYPY_CHECK_RUNNING:
|
||||||
|
from typing import (Optional, Sequence)
|
||||||
|
|
||||||
|
from pip._vendor.packaging.version import _BaseVersion
|
||||||
|
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
return VersionedRequirement(install_req)
|
||||||
|
|
||||||
|
|
||||||
|
class UnnamedRequirement(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 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()]
|
||||||
|
|
||||||
|
def is_satisfied_by(self, candidate):
|
||||||
|
# type: (Candidate) -> bool
|
||||||
|
return candidate is self._get_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(
|
||||||
|
self,
|
||||||
|
finder, # type: PackageFinder
|
||||||
|
):
|
||||||
|
# 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"
|
||||||
|
self._ireq = ireq
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
# type: () -> str
|
||||||
|
canonical_name = canonicalize_name(self._ireq.req.name)
|
||||||
|
return format_name(canonical_name, self._ireq.req.extras)
|
||||||
|
|
||||||
|
def find_matches(
|
||||||
|
self,
|
||||||
|
finder, # type: PackageFinder
|
||||||
|
):
|
||||||
|
# type: (...) -> Sequence[Candidate]
|
||||||
|
found = 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)
|
||||||
|
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...
|
||||||
|
return candidate.version in self._ireq.req.specifier
|
43
tests/unit/resolution_resolvelib/conftest.py
Normal file
43
tests/unit/resolution_resolvelib/conftest.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pip._internal.cli.req_command import RequirementCommand
|
||||||
|
from pip._internal.commands.install import InstallCommand
|
||||||
|
from pip._internal.index.collector import LinkCollector
|
||||||
|
from pip._internal.index.package_finder import PackageFinder
|
||||||
|
# from pip._internal.models.index import PyPI
|
||||||
|
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.req_tracker import get_requirement_tracker
|
||||||
|
from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def finder(data):
|
||||||
|
session = PipSession()
|
||||||
|
scope = SearchScope([str(data.packages)], [])
|
||||||
|
collector = LinkCollector(session, scope)
|
||||||
|
prefs = SelectionPreferences(allow_yanked=False)
|
||||||
|
finder = PackageFinder.create(collector, prefs)
|
||||||
|
yield finder
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def preparer(finder):
|
||||||
|
session = PipSession()
|
||||||
|
rc = InstallCommand("x", "y")
|
||||||
|
o = rc.parse_args([])
|
||||||
|
|
||||||
|
with global_tempdir_manager():
|
||||||
|
with TempDirectory() as tmp:
|
||||||
|
with get_requirement_tracker() as tracker:
|
||||||
|
preparer = RequirementCommand.make_requirement_preparer(
|
||||||
|
tmp,
|
||||||
|
options=o[0],
|
||||||
|
req_tracker=tracker,
|
||||||
|
session=session,
|
||||||
|
finder=finder,
|
||||||
|
use_user_site=False
|
||||||
|
)
|
||||||
|
|
||||||
|
yield preparer
|
75
tests/unit/resolution_resolvelib/test_requirement.py
Normal file
75
tests/unit/resolution_resolvelib/test_requirement.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pip._internal.req.constructors import install_req_from_line
|
||||||
|
from pip._internal.resolution.resolvelib.base import Candidate
|
||||||
|
from pip._internal.resolution.resolvelib.requirements import make_requirement
|
||||||
|
from pip._internal.utils.urls import path_to_url
|
||||||
|
|
||||||
|
# NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver").
|
||||||
|
# This helps select just these tests using pytest's `-k` option, and
|
||||||
|
# keeps test names shorter.
|
||||||
|
|
||||||
|
# Basic tests:
|
||||||
|
# Create a requirement from a project name - "pip"
|
||||||
|
# Create a requirement from a name + version constraint - "pip >= 20.0"
|
||||||
|
# Create a requirement from a wheel filename
|
||||||
|
# Create a requirement from a sdist filename
|
||||||
|
# Create a requirement from a local directory (which has no obvious name!)
|
||||||
|
# Editables
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_cases(data):
|
||||||
|
def data_file(name):
|
||||||
|
return data.packages.joinpath(name)
|
||||||
|
|
||||||
|
def data_url(name):
|
||||||
|
return path_to_url(data_file(name))
|
||||||
|
|
||||||
|
test_cases = [
|
||||||
|
# requirement, name, matches
|
||||||
|
# Version specifiers
|
||||||
|
("simple", "simple", 3),
|
||||||
|
("simple>1.0", "simple", 2),
|
||||||
|
("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),
|
||||||
|
# SDists
|
||||||
|
# TODO: sdists should have a name
|
||||||
|
(data_file("simple-1.0.tar.gz"), "", 1),
|
||||||
|
(data_url("simple-1.0.tar.gz"), "", 1),
|
||||||
|
# TODO: directory, editables
|
||||||
|
]
|
||||||
|
|
||||||
|
yield test_cases
|
||||||
|
|
||||||
|
|
||||||
|
def req_from_line(line):
|
||||||
|
return make_requirement(install_req_from_line(line))
|
||||||
|
|
||||||
|
|
||||||
|
def test_rlr_requirement_has_name(test_cases):
|
||||||
|
"""All requirements should have a name"""
|
||||||
|
for requirement, name, matches in test_cases:
|
||||||
|
req = req_from_line(requirement)
|
||||||
|
assert req.name == name
|
||||||
|
|
||||||
|
|
||||||
|
def test_rlr_correct_number_of_matches(test_cases, finder):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
def test_rlr_candidates_match_requirement(test_cases, finder):
|
||||||
|
"""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):
|
||||||
|
assert isinstance(c, Candidate)
|
||||||
|
assert req.is_satisfied_by(c)
|
Loading…
Reference in a new issue