From 0773602680479d01014712777d5c50d4ce8cad1e Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sat, 20 Sep 2025 23:33:02 -0400 Subject: [PATCH 1/7] Add hint when resolution impossible causes have no versions --- .../resolution/resolvelib/factory.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f23e4cd6258..1a29d54ec21 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -711,6 +711,13 @@ def _report_single_requirement_conflict( return DistributionNotFound(f"No matching distribution found for {req}") + def _has_any_candidates(self, project_name: str) -> bool: + """ + Check if there are any candidates available for the project name. + """ + candidates = self._finder.find_all_candidates(project_name) + return any(not c.link.is_yanked for c in candidates) + def get_installation_error( self, e: ResolutionImpossible[Requirement, Candidate], @@ -796,6 +803,22 @@ def describe_trigger(parent: Candidate) -> str: spec = constraints[key].specifier msg += f"\n The user requested (constraint) {key}{spec}" + # Check for causes that had no candidates + causes = set() + for req, _ in e.causes: + causes.add(req.name) + + no_candidates = {c for c in causes if not self._has_any_candidates(c)} + if no_candidates: + msg = ( + msg + + "\n\n" + + "Some conflict cause(s) have no " + + "available versions for your environment:" + + "\n " + + "\n ".join(sorted(no_candidates)) + ) + msg = ( msg + "\n\n" From 1a2599ca77d574a95238d1f5f49d3b92a824ece2 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Sat, 20 Sep 2025 23:40:00 -0400 Subject: [PATCH 2/7] NEWS ENTRY --- news/13588.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/13588.feature.rst diff --git a/news/13588.feature.rst b/news/13588.feature.rst new file mode 100644 index 00000000000..87c56881bae --- /dev/null +++ b/news/13588.feature.rst @@ -0,0 +1 @@ +On ``ResolutionImpossible`` errors, include a note about causes with no candidates. From d76518544ad5db2ae197a1151dbab409af06e8ac Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Mon, 22 Sep 2025 20:43:22 -0400 Subject: [PATCH 3/7] Don't exclude yanked links on impossible project check --- src/pip/_internal/resolution/resolvelib/factory.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 1a29d54ec21..b96d16f0993 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -715,8 +715,7 @@ def _has_any_candidates(self, project_name: str) -> bool: """ Check if there are any candidates available for the project name. """ - candidates = self._finder.find_all_candidates(project_name) - return any(not c.link.is_yanked for c in candidates) + return any(self._finder.find_all_candidates(project_name)) def get_installation_error( self, From e043f06622b858741086ca1661993448b37920aa Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Mon, 22 Sep 2025 20:43:38 -0400 Subject: [PATCH 4/7] Update project check wording --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b96d16f0993..f3f462384db 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -812,8 +812,8 @@ def describe_trigger(parent: Candidate) -> str: msg = ( msg + "\n\n" - + "Some conflict cause(s) have no " - + "available versions for your environment:" + + "Additionally, some conflict cause(s) have no " + + "available versions for your environment at all:" + "\n " + "\n ".join(sorted(no_candidates)) ) From da78b5106b2ddaa2e3e83835e98f379e0f4457c6 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Mon, 22 Sep 2025 20:44:56 -0400 Subject: [PATCH 5/7] Add test for impossible hint when project has no versions available at all. --- tests/functional/test_new_resolver_errors.py | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index ebca917d0a5..e8a134f4ecf 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -6,6 +6,7 @@ create_basic_wheel_for_package, create_test_package_with_setup, ) +from tests.lib.wheel import make_wheel def test_new_resolver_conflict_requirements_file( @@ -132,3 +133,61 @@ def test_new_resolver_checks_requires_python_before_dependencies( # Setuptools produces wheels with normalized names. assert "pkg_dep" not in result.stderr, str(result) assert "pkg_dep" not in result.stdout, str(result) + + +def test_new_resolver_no_versions_available_hint(script: PipTestEnvironment) -> None: + """ + Test hint that no package candidate is available at all, + when ResolutionImpossible occurs. + """ + wheel_house = script.scratch_path.joinpath("wheelhouse") + wheel_house.mkdir() + + incompatible_dep_wheel = make_wheel( + name="incompatible-dep", + version="1.0.0", + wheel_metadata_updates={"Tag": ["py3-none-fakeplat"]}, + ) + incompatible_dep_wheel.save_to( + wheel_house.joinpath("incompatible_dep-1.0.0-py3-none-fakeplat.whl") + ) + + # Create multiple versions of a package that depend on the incompatible dependency + requesting_pkg_v1 = make_wheel( + name="requesting-pkg", + version="1.0.0", + metadata_updates={"Requires-Dist": ["incompatible-dep==1.0.0"]}, + ) + requesting_pkg_v1.save_to( + wheel_house.joinpath("requesting_pkg-1.0.0-py2.py3-none-any.whl") + ) + + requesting_pkg_v2 = make_wheel( + name="requesting-pkg", + version="2.0.0", + metadata_updates={"Requires-Dist": ["incompatible-dep==1.0.0"]}, + ) + requesting_pkg_v2.save_to( + wheel_house.joinpath("requesting_pkg-2.0.0-py2.py3-none-any.whl") + ) + + # Attempt to install the requesting package + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", + str(wheel_house), + "requesting-pkg", + expect_error=True, + ) + + # Check that ResolutionImpossible error occurred + assert "ResolutionImpossible" in result.stderr, str(result) + + # Check that the new hint message is present + assert ( + "Additionally, some conflict cause(s) have no available " + "versions for your environment at all:\n" + " incompatible-dep\n" in result.stdout + ), str(result) From de6ec7752d8e70f3bd1725a0a229561614671924 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Tue, 23 Sep 2025 00:20:32 -0400 Subject: [PATCH 6/7] Grammatically cleaner version of hint --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- tests/functional/test_new_resolver_errors.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f3f462384db..b75ff17ac6e 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -812,8 +812,8 @@ def describe_trigger(parent: Candidate) -> str: msg = ( msg + "\n\n" - + "Additionally, some conflict cause(s) have no " - + "available versions for your environment at all:" + + "Additionally, some conflict causes have no " + + "available versions for your environment:" + "\n " + "\n ".join(sorted(no_candidates)) ) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index e8a134f4ecf..883b08d72f7 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -187,7 +187,7 @@ def test_new_resolver_no_versions_available_hint(script: PipTestEnvironment) -> # Check that the new hint message is present assert ( - "Additionally, some conflict cause(s) have no available " - "versions for your environment at all:\n" + "Additionally, some conflict causes have no " + "available versions for your environment:\n" " incompatible-dep\n" in result.stdout ), str(result) From 6e3b61ddf9f6f97bba4dbde6a7906791268aacc2 Mon Sep 17 00:00:00 2001 From: Damian Shaw Date: Tue, 23 Sep 2025 19:28:14 -0400 Subject: [PATCH 7/7] Reword hint to make clear it's the whole project that has no "matching distributions" --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- tests/functional/test_new_resolver_errors.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b75ff17ac6e..87c586c6ee1 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -812,8 +812,8 @@ def describe_trigger(parent: Candidate) -> str: msg = ( msg + "\n\n" - + "Additionally, some conflict causes have no " - + "available versions for your environment:" + + "Additionally, some projects in these conflicts have no " + + "matching distributions available for your environment:" + "\n " + "\n ".join(sorted(no_candidates)) ) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index 883b08d72f7..d136a108d75 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -187,7 +187,7 @@ def test_new_resolver_no_versions_available_hint(script: PipTestEnvironment) -> # Check that the new hint message is present assert ( - "Additionally, some conflict causes have no " - "available versions for your environment:\n" + "Additionally, some projects in these conflicts have no " + "matching distributions available for your environment:\n" " incompatible-dep\n" in result.stdout ), str(result)