From 9b3cd280fdafcfa4a3d00a0a26594566609de535 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 4 Nov 2020 00:05:26 +0800 Subject: [PATCH 1/5] Add failing test --- tests/functional/test_new_resolver.py | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 374d37aee..5e36ec498 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1132,3 +1132,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") From d589795834bd36d057f51b2b65643f8c44726e1f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 4 Nov 2020 00:29:19 +0800 Subject: [PATCH 2/5] Allow constraining an explicit requirement --- src/pip/_internal/resolution/resolvelib/base.py | 7 +++++++ src/pip/_internal/resolution/resolvelib/factory.py | 10 ++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 7c09cd70b..e2edbe9f4 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -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 diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c65cb7f76..f4177d981 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -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): From 0f6750c98cd5391c00517f5c88cbc1c5f322dfc6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 4 Nov 2020 01:30:12 +0800 Subject: [PATCH 3/5] Modify old tests to accomodate restriction removal --- tests/functional/test_install_reqs.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 575adbe15..c5985243b 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -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( From 9efafb186fd907b308be5e06223213476a6b7dce Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sat, 21 Nov 2020 21:53:37 +0800 Subject: [PATCH 4/5] Implement __str__ for debuggability --- .../resolution/resolvelib/candidates.py | 17 +++++++++++++++++ .../resolution/resolvelib/requirements.py | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 65585fd36..1fc2ff479 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -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 diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index bc1061f43..25cddceaf 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -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 From c3670b36cbaea0a756bcb0b1f96e56faedacb3b2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Sat, 21 Nov 2020 22:13:18 +0800 Subject: [PATCH 5/5] 2020 resolver can constrain path/URL to versions --- tests/functional/test_new_resolver.py | 28 ++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 5e36ec498..45e1a0347 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -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):