Implement the resolvelib Requirement class

This commit is contained in:
Paul Moore 2020-03-09 09:06:32 +00:00
parent f981facb70
commit 9b10b93503
5 changed files with 310 additions and 0 deletions

View 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")

View 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

View 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

View 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)