Skip to content
1 change: 1 addition & 0 deletions news/13588.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On ``ResolutionImpossible`` errors, include a note about causes with no candidates.
22 changes: 22 additions & 0 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,12 @@ 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.
"""
return any(self._finder.find_all_candidates(project_name))

def get_installation_error(
self,
e: ResolutionImpossible[Requirement, Candidate],
Expand Down Expand Up @@ -796,6 +802,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"
+ "Additionally, some projects in these conflicts have no "
+ "matching distributions available for your environment:"
+ "\n "
+ "\n ".join(sorted(no_candidates))
)

msg = (
msg
+ "\n\n"
Expand Down
59 changes: 59 additions & 0 deletions tests/functional/test_new_resolver_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 projects in these conflicts have no "
"matching distributions available for your environment:\n"
" incompatible-dep\n" in result.stdout
), str(result)