Skip to content

Commit 8438271

Browse files
authored
Merge pull request #9096 from uranusjr/new-resolver-constrainting-extraed
Constrainting an extra-ed dependency
2 parents aa847ea + c3670b3 commit 8438271

File tree

6 files changed

+121
-18
lines changed

6 files changed

+121
-18
lines changed

src/pip/_internal/resolution/resolvelib/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def __and__(self, other):
5858
hashes = self.hashes & other.hashes(trust_internet=False)
5959
return Constraint(specifier, hashes)
6060

61+
def is_satisfied_by(self, candidate):
62+
# type: (Candidate) -> bool
63+
# We can safely always allow prereleases here since PackageFinder
64+
# already implements the prerelease logic, and would have filtered out
65+
# prerelease candidates if the user does not expect them.
66+
return self.specifier.contains(candidate.version, prereleases=True)
67+
6168

6269
class Requirement(object):
6370
@property

src/pip/_internal/resolution/resolvelib/candidates.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ def __init__(
143143
self._version = version
144144
self._dist = None # type: Optional[Distribution]
145145

146+
def __str__(self):
147+
# type: () -> str
148+
return "{} {}".format(self.name, self.version)
149+
146150
def __repr__(self):
147151
# type: () -> str
148152
return "{class_name}({link!r})".format(
@@ -359,6 +363,10 @@ def __init__(
359363
skip_reason = "already satisfied"
360364
factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
361365

366+
def __str__(self):
367+
# type: () -> str
368+
return str(self.dist)
369+
362370
def __repr__(self):
363371
# type: () -> str
364372
return "{class_name}({distribution!r})".format(
@@ -445,6 +453,11 @@ def __init__(
445453
self.base = base
446454
self.extras = extras
447455

456+
def __str__(self):
457+
# type: () -> str
458+
name, rest = str(self.base).split(" ", 1)
459+
return "{}[{}] {}".format(name, ",".join(self.extras), rest)
460+
448461
def __repr__(self):
449462
# type: () -> str
450463
return "{class_name}(base={base!r}, extras={extras!r})".format(
@@ -554,6 +567,10 @@ def __init__(self, py_version_info):
554567
# only one RequiresPythonCandidate in a resolution, i.e. the host Python.
555568
# The built-in object.__eq__() and object.__ne__() do exactly what we want.
556569

570+
def __str__(self):
571+
# type: () -> str
572+
return "Python {}".format(self._version)
573+
557574
@property
558575
def name(self):
559576
# type: () -> str

src/pip/_internal/resolution/resolvelib/factory.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,16 +235,10 @@ def find_candidates(
235235
prefers_installed,
236236
)
237237

238-
if constraint:
239-
name = explicit_candidates.pop().name
240-
raise InstallationError(
241-
"Could not satisfy constraints for {!r}: installation from "
242-
"path or url cannot be constrained to a version".format(name)
243-
)
244-
245238
return (
246239
c for c in explicit_candidates
247-
if all(req.is_satisfied_by(c) for req in requirements)
240+
if constraint.is_satisfied_by(c)
241+
and all(req.is_satisfied_by(c) for req in requirements)
248242
)
249243

250244
def make_requirement_from_install_req(self, ireq, requested_extras):

src/pip/_internal/resolution/resolvelib/requirements.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def __init__(self, candidate):
1717
# type: (Candidate) -> None
1818
self.candidate = candidate
1919

20+
def __str__(self):
21+
# type: () -> str
22+
return str(self.candidate)
23+
2024
def __repr__(self):
2125
# type: () -> str
2226
return "{class_name}({candidate!r})".format(
@@ -106,6 +110,10 @@ def __init__(self, specifier, match):
106110
self.specifier = specifier
107111
self._candidate = match
108112

113+
def __str__(self):
114+
# type: () -> str
115+
return "Python {}".format(self.specifier)
116+
109117
def __repr__(self):
110118
# type: () -> str
111119
return "{class_name}({specifier!r})".format(
@@ -120,7 +128,7 @@ def name(self):
120128

121129
def format_for_error(self):
122130
# type: () -> str
123-
return "Python " + str(self.specifier)
131+
return str(self)
124132

125133
def get_candidate_lookup(self):
126134
# type: () -> CandidateLookup

tests/functional/test_install_reqs.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,11 @@ def test_constraints_only_causes_error(script, data):
341341
assert 'installed requiresupper' not in result.stdout
342342

343343

344-
def test_constraints_local_editable_install_causes_error(script, data):
344+
def test_constraints_local_editable_install_causes_error(
345+
script,
346+
data,
347+
resolver_variant,
348+
):
345349
script.scratch_path.joinpath("constraints.txt").write_text(
346350
"singlemodule==0.0.0"
347351
)
@@ -350,7 +354,11 @@ def test_constraints_local_editable_install_causes_error(script, data):
350354
'install', '--no-index', '-f', data.find_links, '-c',
351355
script.scratch_path / 'constraints.txt', '-e',
352356
to_install, expect_error=True)
353-
assert 'Could not satisfy constraints for' in result.stderr
357+
if resolver_variant == "legacy-resolver":
358+
assert 'Could not satisfy constraints' in result.stderr, str(result)
359+
else:
360+
# Because singlemodule only has 0.0.1 available.
361+
assert 'No matching distribution found' in result.stderr, str(result)
354362

355363

356364
@pytest.mark.network
@@ -362,7 +370,11 @@ def test_constraints_local_editable_install_pep518(script, data):
362370
'install', '--no-index', '-f', data.find_links, '-e', to_install)
363371

364372

365-
def test_constraints_local_install_causes_error(script, data):
373+
def test_constraints_local_install_causes_error(
374+
script,
375+
data,
376+
resolver_variant,
377+
):
366378
script.scratch_path.joinpath("constraints.txt").write_text(
367379
"singlemodule==0.0.0"
368380
)
@@ -371,7 +383,11 @@ def test_constraints_local_install_causes_error(script, data):
371383
'install', '--no-index', '-f', data.find_links, '-c',
372384
script.scratch_path / 'constraints.txt',
373385
to_install, expect_error=True)
374-
assert 'Could not satisfy constraints for' in result.stderr
386+
if resolver_variant == "legacy-resolver":
387+
assert 'Could not satisfy constraints' in result.stderr, str(result)
388+
else:
389+
# Because singlemodule only has 0.0.1 available.
390+
assert 'No matching distribution found' in result.stderr, str(result)
375391

376392

377393
def test_constraints_constrain_to_local_editable(

tests/functional/test_new_resolver.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -712,22 +712,40 @@ def test_new_resolver_constraint_on_dependency(script):
712712
assert_installed(script, dep="2.0")
713713

714714

715-
def test_new_resolver_constraint_on_path(script):
715+
@pytest.mark.parametrize(
716+
"constraint_version, expect_error, message",
717+
[
718+
("1.0", True, "ERROR: No matching distribution found for foo 2.0"),
719+
("2.0", False, "Successfully installed foo-2.0"),
720+
],
721+
)
722+
def test_new_resolver_constraint_on_path_empty(
723+
script,
724+
constraint_version,
725+
expect_error,
726+
message,
727+
):
728+
"""A path requirement can be filtered by a constraint.
729+
"""
716730
setup_py = script.scratch_path / "setup.py"
717731
text = "from setuptools import setup\nsetup(name='foo', version='2.0')"
718732
setup_py.write_text(text)
733+
719734
constraints_txt = script.scratch_path / "constraints.txt"
720-
constraints_txt.write_text("foo==1.0")
735+
constraints_txt.write_text("foo=={}".format(constraint_version))
736+
721737
result = script.pip(
722738
"install",
723739
"--no-cache-dir", "--no-index",
724740
"-c", constraints_txt,
725741
str(script.scratch_path),
726-
expect_error=True,
742+
expect_error=expect_error,
727743
)
728744

729-
msg = "installation from path or url cannot be constrained to a version"
730-
assert msg in result.stderr, str(result)
745+
if expect_error:
746+
assert message in result.stderr, str(result)
747+
else:
748+
assert message in result.stdout, str(result)
731749

732750

733751
def test_new_resolver_constraint_only_marker_match(script):
@@ -1132,3 +1150,46 @@ def test_new_resolver_check_wheel_version_normalized(
11321150
"simple"
11331151
)
11341152
assert_installed(script, simple="0.1.0+local.1")
1153+
1154+
1155+
def test_new_resolver_contraint_on_dep_with_extra(script):
1156+
create_basic_wheel_for_package(
1157+
script,
1158+
name="simple",
1159+
version="1",
1160+
depends=["dep[x]"],
1161+
)
1162+
create_basic_wheel_for_package(
1163+
script,
1164+
name="dep",
1165+
version="1",
1166+
extras={"x": ["depx==1"]},
1167+
)
1168+
create_basic_wheel_for_package(
1169+
script,
1170+
name="dep",
1171+
version="2",
1172+
extras={"x": ["depx==2"]},
1173+
)
1174+
create_basic_wheel_for_package(
1175+
script,
1176+
name="depx",
1177+
version="1",
1178+
)
1179+
create_basic_wheel_for_package(
1180+
script,
1181+
name="depx",
1182+
version="2",
1183+
)
1184+
1185+
constraints_txt = script.scratch_path / "constraints.txt"
1186+
constraints_txt.write_text("dep==1")
1187+
1188+
script.pip(
1189+
"install",
1190+
"--no-cache-dir", "--no-index",
1191+
"--find-links", script.scratch_path,
1192+
"--constraint", constraints_txt,
1193+
"simple",
1194+
)
1195+
assert_installed(script, simple="1", dep="1", depx="1")

0 commit comments

Comments
 (0)