Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
31 changes: 31 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,21 @@ 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.find_candidates(
project_name,
requirements={project_name: []},
incompatibilities={},
constraint=Constraint.empty(),
prefers_installed=True,
is_satisfied_by=lambda r, c: True,
)
)

def get_installation_error(
self,
e: ResolutionImpossible[Requirement, Candidate],
Expand Down Expand Up @@ -796,6 +811,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 packages 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 packages in these conflicts have no "
"matching distributions available for your environment:\n"
" incompatible-dep\n" in result.stdout
), str(result)