Merge pull request #8170 from pfmoore/nr_constraints2

New Resolver: Second try at implementing constraints
This commit is contained in:
Paul Moore 2020-05-07 12:40:49 +01:00 committed by GitHub
commit 94f2dc435b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 22 deletions

View File

@ -6,6 +6,7 @@ if MYPY_CHECK_RUNNING:
from typing import Optional, Sequence, Set
from pip._internal.req.req_install import InstallRequirement
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.packaging.version import _BaseVersion
@ -23,8 +24,8 @@ class Requirement(object):
# type: () -> str
raise NotImplementedError("Subclass should override")
def find_matches(self):
# type: () -> Sequence[Candidate]
def find_matches(self, constraint):
# type: (SpecifierSet) -> Sequence[Candidate]
raise NotImplementedError("Subclass should override")
def is_satisfied_by(self, candidate):

View File

@ -1,9 +1,10 @@
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib.providers import AbstractProvider
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Any, Optional, Sequence, Tuple, Union
from typing import Any, Dict, Optional, Sequence, Tuple, Union
from pip._internal.req.req_install import InstallRequirement
@ -15,10 +16,12 @@ class PipProvider(AbstractProvider):
def __init__(
self,
factory, # type: Factory
constraints, # type: Dict[str, SpecifierSet]
ignore_dependencies, # type: bool
):
# type: (...) -> None
self._factory = factory
self._constraints = constraints
self._ignore_dependencies = ignore_dependencies
def get_install_requirement(self, c):
@ -41,7 +44,8 @@ class PipProvider(AbstractProvider):
def find_matches(self, requirement):
# type: (Requirement) -> Sequence[Candidate]
return requirement.find_matches()
constraint = self._constraints.get(requirement.name, SpecifierSet())
return requirement.find_matches(constraint)
def is_satisfied_by(self, requirement, candidate):
# type: (Requirement, Candidate) -> bool

View File

@ -1,5 +1,6 @@
from pip._vendor.packaging.utils import canonicalize_name
from pip._internal.exceptions import InstallationError
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from .base import Requirement, format_name
@ -33,8 +34,14 @@ class ExplicitRequirement(Requirement):
# No need to canonicalise - the candidate did this
return self.candidate.name
def find_matches(self):
# type: () -> Sequence[Candidate]
def find_matches(self, constraint):
# type: (SpecifierSet) -> Sequence[Candidate]
if len(constraint) > 0:
raise InstallationError(
"Could not satisfy constraints for '{}': "
"installation from path or url cannot be "
"constrained to a version".format(self.name)
)
return [self.candidate]
def is_satisfied_by(self, candidate):
@ -67,10 +74,15 @@ class SpecifierRequirement(Requirement):
canonical_name = canonicalize_name(self._ireq.req.name)
return format_name(canonical_name, self.extras)
def find_matches(self):
# type: () -> Sequence[Candidate]
it = self._factory.iter_found_candidates(self._ireq, self.extras)
return list(it)
def find_matches(self, constraint):
# type: (SpecifierSet) -> Sequence[Candidate]
return [
c
for c in self._factory.iter_found_candidates(
self._ireq, self.extras
)
if constraint.contains(c.version, prereleases=True)
]
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
@ -104,8 +116,10 @@ class RequiresPythonRequirement(Requirement):
# type: () -> str
return self._candidate.name
def find_matches(self):
# type: () -> Sequence[Candidate]
def find_matches(self, constraint):
# type: (SpecifierSet) -> Sequence[Candidate]
assert len(constraint) == 0, \
"RequiresPythonRequirement cannot have constraints"
if self._candidate.version in self.specifier:
return [self._candidate]
return []

View File

@ -17,6 +17,7 @@ from .factory import Factory
if MYPY_CHECK_RUNNING:
from typing import Dict, List, Optional, Set, Tuple
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib.resolvers import Result
from pip._vendor.resolvelib.structs import Graph
@ -69,22 +70,30 @@ class Resolver(BaseResolver):
# The persistent state that we care about is `root_reqs`.
assert len(self.factory.root_reqs) == 0, "Factory is being re-used"
# FIXME: Implement constraints.
if any(r.constraint for r in root_reqs):
raise InstallationError("Constraints are not yet supported.")
constraints = {} # type: Dict[str, SpecifierSet]
requirements = []
for req in root_reqs:
if req.constraint:
assert req.name
assert req.specifier
name = canonicalize_name(req.name)
if name in constraints:
constraints[name] = constraints[name] & req.specifier
else:
constraints[name] = req.specifier
else:
requirements.append(
self.factory.make_requirement_from_install_req(req)
)
provider = PipProvider(
factory=self.factory,
constraints=constraints,
ignore_dependencies=self.ignore_dependencies,
)
reporter = BaseReporter()
resolver = RLResolver(provider, reporter)
requirements = [
self.factory.make_requirement_from_install_req(r)
for r in root_reqs
]
try:
self._result = resolver.resolve(requirements)

View File

@ -531,6 +531,59 @@ def test_new_resolver_handles_prerelease(
assert_installed(script, pkg=expected_version)
def test_new_resolver_constraints(script):
create_basic_wheel_for_package(script, "pkg", "1.0")
create_basic_wheel_for_package(script, "pkg", "2.0")
create_basic_wheel_for_package(script, "pkg", "3.0")
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text("pkg<2.0\nconstraint_only<1.0")
script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"-c", constraints_file,
"pkg"
)
assert_installed(script, pkg="1.0")
assert_not_installed(script, "constraint_only")
def test_new_resolver_constraint_on_dependency(script):
create_basic_wheel_for_package(script, "base", "1.0", depends=["dep"])
create_basic_wheel_for_package(script, "dep", "1.0")
create_basic_wheel_for_package(script, "dep", "2.0")
create_basic_wheel_for_package(script, "dep", "3.0")
constraints_file = script.scratch_path / "constraints.txt"
constraints_file.write_text("dep==2.0")
script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"-c", constraints_file,
"base"
)
assert_installed(script, base="1.0")
assert_installed(script, dep="2.0")
def test_new_resolver_constraint_on_path(script):
setup_py = script.scratch_path / "setup.py"
text = "from setuptools import setup\nsetup(name='foo', version='2.0')"
setup_py.write_text(text)
constraints_txt = script.scratch_path / "constraints.txt"
constraints_txt.write_text("foo==1.0")
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"-c", constraints_txt,
str(script.scratch_path),
expect_error=True,
)
msg = "installation from path or url cannot be constrained to a version"
assert msg in result.stderr, str(result)
def test_new_resolver_upgrade_needs_option(script):
# Install pkg 1.0.0
create_basic_wheel_for_package(script, "pkg", "1.0.0")

View File

@ -64,5 +64,6 @@ def factory(finder, preparer):
def provider(factory):
yield PipProvider(
factory=factory,
constraints={},
ignore_dependencies=False,
)

View File

@ -1,4 +1,5 @@
import pytest
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._vendor.resolvelib import BaseReporter, Resolver
from pip._internal.resolution.resolvelib.base import Candidate
@ -58,14 +59,14 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory):
"""Requirements should return the correct number of candidates"""
for spec, name, matches in test_cases:
req = factory.make_requirement_from_spec(spec, comes_from=None)
assert len(req.find_matches()) == matches
assert len(req.find_matches(SpecifierSet())) == matches
def test_new_resolver_candidates_match_requirement(test_cases, factory):
"""Candidates returned from find_matches should satisfy the requirement"""
for spec, name, matches in test_cases:
req = factory.make_requirement_from_spec(spec, comes_from=None)
for c in req.find_matches():
for c in req.find_matches(SpecifierSet()):
assert isinstance(c, Candidate)
assert req.is_satisfied_by(c)