mirror of https://github.com/pypa/pip
Merge pull request #8170 from pfmoore/nr_constraints2
New Resolver: Second try at implementing constraints
This commit is contained in:
commit
94f2dc435b
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -64,5 +64,6 @@ def factory(finder, preparer):
|
|||
def provider(factory):
|
||||
yield PipProvider(
|
||||
factory=factory,
|
||||
constraints={},
|
||||
ignore_dependencies=False,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue