-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement the new resolver's Requirement class #7843
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 changes: 141 additions & 0 deletions
141
src/pip/_internal/resolution/resolvelib/requirements.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there cases where
name
wouldn’t be called? I can’t think of any. I am tempted to suggest alwaysNamedRequirement
instead, by getting the candidate very eagerly inmake_requirement
and assigning it to theInstallRequirement
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I.e. something like
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but without a "proper" candidate object, I can't do this without the tests failing (see the test that currently asserts that a sdist has a name of ""). So I plan on addressing this in one of the follow up PRs (either the one that implements candidates, or more likely a further PR that tidies up all the links).
In hindsight, I'm not convinced that splitting up the original change from #7799 was worthwhile, it's created a lot of busy-work, and I don't think the resulting PRs are that much more reviewable. But adding the tests was (IMO) a definite benefit, so I'm not going to worry too much..