mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Merge pull request #7843 from pfmoore/resolvelib_requirement
Implement the new resolver's Requirement class
This commit is contained in:
commit
10f41571b1
5 changed files with 310 additions and 0 deletions
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