Merge pull request #9096 from uranusjr/new-resolver-constrainting-extraed

Constrainting an extra-ed dependency
This commit is contained in:
Pradyun Gedam 2020-11-23 10:57:39 +00:00 committed by GitHub
commit 84382715f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 18 deletions

View File

@ -58,6 +58,13 @@ class Constraint(object):
hashes = self.hashes & other.hashes(trust_internet=False)
return Constraint(specifier, hashes)
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
# We can safely always allow prereleases here since PackageFinder
# already implements the prerelease logic, and would have filtered out
# prerelease candidates if the user does not expect them.
return self.specifier.contains(candidate.version, prereleases=True)
class Requirement(object):
@property

View File

@ -143,6 +143,10 @@ class _InstallRequirementBackedCandidate(Candidate):
self._version = version
self._dist = None # type: Optional[Distribution]
def __str__(self):
# type: () -> str
return "{} {}".format(self.name, self.version)
def __repr__(self):
# type: () -> str
return "{class_name}({link!r})".format(
@ -359,6 +363,10 @@ class AlreadyInstalledCandidate(Candidate):
skip_reason = "already satisfied"
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
def __str__(self):
# type: () -> str
return str(self.dist)
def __repr__(self):
# type: () -> str
return "{class_name}({distribution!r})".format(
@ -445,6 +453,11 @@ class ExtrasCandidate(Candidate):
self.base = base
self.extras = extras
def __str__(self):
# type: () -> str
name, rest = str(self.base).split(" ", 1)
return "{}[{}] {}".format(name, ",".join(self.extras), rest)
def __repr__(self):
# type: () -> str
return "{class_name}(base={base!r}, extras={extras!r})".format(
@ -554,6 +567,10 @@ class RequiresPythonCandidate(Candidate):
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
def __str__(self):
# type: () -> str
return "Python {}".format(self._version)
@property
def name(self):
# type: () -> str

View File

@ -235,16 +235,10 @@ class Factory(object):
prefers_installed,
)
if constraint:
name = explicit_candidates.pop().name
raise InstallationError(
"Could not satisfy constraints for {!r}: installation from "
"path or url cannot be constrained to a version".format(name)
)
return (
c for c in explicit_candidates
if all(req.is_satisfied_by(c) for req in requirements)
if constraint.is_satisfied_by(c)
and all(req.is_satisfied_by(c) for req in requirements)
)
def make_requirement_from_install_req(self, ireq, requested_extras):

View File

@ -17,6 +17,10 @@ class ExplicitRequirement(Requirement):
# type: (Candidate) -> None
self.candidate = candidate
def __str__(self):
# type: () -> str
return str(self.candidate)
def __repr__(self):
# type: () -> str
return "{class_name}({candidate!r})".format(
@ -106,6 +110,10 @@ class RequiresPythonRequirement(Requirement):
self.specifier = specifier
self._candidate = match
def __str__(self):
# type: () -> str
return "Python {}".format(self.specifier)
def __repr__(self):
# type: () -> str
return "{class_name}({specifier!r})".format(
@ -120,7 +128,7 @@ class RequiresPythonRequirement(Requirement):
def format_for_error(self):
# type: () -> str
return "Python " + str(self.specifier)
return str(self)
def get_candidate_lookup(self):
# type: () -> CandidateLookup

View File

@ -341,7 +341,11 @@ def test_constraints_only_causes_error(script, data):
assert 'installed requiresupper' not in result.stdout
def test_constraints_local_editable_install_causes_error(script, data):
def test_constraints_local_editable_install_causes_error(
script,
data,
resolver_variant,
):
script.scratch_path.joinpath("constraints.txt").write_text(
"singlemodule==0.0.0"
)
@ -350,7 +354,11 @@ def test_constraints_local_editable_install_causes_error(script, data):
'install', '--no-index', '-f', data.find_links, '-c',
script.scratch_path / 'constraints.txt', '-e',
to_install, expect_error=True)
assert 'Could not satisfy constraints for' in result.stderr
if resolver_variant == "legacy-resolver":
assert 'Could not satisfy constraints' in result.stderr, str(result)
else:
# Because singlemodule only has 0.0.1 available.
assert 'No matching distribution found' in result.stderr, str(result)
@pytest.mark.network
@ -362,7 +370,11 @@ def test_constraints_local_editable_install_pep518(script, data):
'install', '--no-index', '-f', data.find_links, '-e', to_install)
def test_constraints_local_install_causes_error(script, data):
def test_constraints_local_install_causes_error(
script,
data,
resolver_variant,
):
script.scratch_path.joinpath("constraints.txt").write_text(
"singlemodule==0.0.0"
)
@ -371,7 +383,11 @@ def test_constraints_local_install_causes_error(script, data):
'install', '--no-index', '-f', data.find_links, '-c',
script.scratch_path / 'constraints.txt',
to_install, expect_error=True)
assert 'Could not satisfy constraints for' in result.stderr
if resolver_variant == "legacy-resolver":
assert 'Could not satisfy constraints' in result.stderr, str(result)
else:
# Because singlemodule only has 0.0.1 available.
assert 'No matching distribution found' in result.stderr, str(result)
def test_constraints_constrain_to_local_editable(

View File

@ -712,22 +712,40 @@ def test_new_resolver_constraint_on_dependency(script):
assert_installed(script, dep="2.0")
def test_new_resolver_constraint_on_path(script):
@pytest.mark.parametrize(
"constraint_version, expect_error, message",
[
("1.0", True, "ERROR: No matching distribution found for foo 2.0"),
("2.0", False, "Successfully installed foo-2.0"),
],
)
def test_new_resolver_constraint_on_path_empty(
script,
constraint_version,
expect_error,
message,
):
"""A path requirement can be filtered by a constraint.
"""
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")
constraints_txt.write_text("foo=={}".format(constraint_version))
result = script.pip(
"install",
"--no-cache-dir", "--no-index",
"-c", constraints_txt,
str(script.scratch_path),
expect_error=True,
expect_error=expect_error,
)
msg = "installation from path or url cannot be constrained to a version"
assert msg in result.stderr, str(result)
if expect_error:
assert message in result.stderr, str(result)
else:
assert message in result.stdout, str(result)
def test_new_resolver_constraint_only_marker_match(script):
@ -1132,3 +1150,46 @@ def test_new_resolver_check_wheel_version_normalized(
"simple"
)
assert_installed(script, simple="0.1.0+local.1")
def test_new_resolver_contraint_on_dep_with_extra(script):
create_basic_wheel_for_package(
script,
name="simple",
version="1",
depends=["dep[x]"],
)
create_basic_wheel_for_package(
script,
name="dep",
version="1",
extras={"x": ["depx==1"]},
)
create_basic_wheel_for_package(
script,
name="dep",
version="2",
extras={"x": ["depx==2"]},
)
create_basic_wheel_for_package(
script,
name="depx",
version="1",
)
create_basic_wheel_for_package(
script,
name="depx",
version="2",
)
constraints_txt = script.scratch_path / "constraints.txt"
constraints_txt.write_text("dep==1")
script.pip(
"install",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--constraint", constraints_txt,
"simple",
)
assert_installed(script, simple="1", dep="1", depx="1")