Skip to content
Merged
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
13 changes: 10 additions & 3 deletions nab-python/src/nab_python/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -781,8 +781,8 @@ def __init__( # noqa: PLR0913, PLR0915 - resolver config is wide; bundling all

# The dep range each rejected candidate declared for the blocker,
# unioned per group. Feeds the membership widening of the flushed
# blocker term; the range-keyed union also feeds the no-versions
# message.
# blocker term, and the no-versions message, which names the range the
# candidate declared rather than negating the blocker it hit.
self.pending_decision_dep_ranges: defaultdict[
tuple[str, str, Version], _lookahead.DepRangeUnion
] = defaultdict(_lookahead.DepRangeUnion.zero)
Expand Down Expand Up @@ -1780,7 +1780,14 @@ def _capture_lookahead_blockers(self, normalized: str) -> list[str]:
for cand, blocker_pkg, blocker_version in self.pending_blocks:
if cand != normalized:
continue
out.append(f"requires {blocker_pkg} != {blocker_version}")
dep_range = self.pending_decision_dep_ranges[
(cand, blocker_pkg, blocker_version)
].union
out.append(
f"requires {blocker_pkg} in {self.format_range(dep_range)}"
" but solution has it in "
f"{self.format_range(VersionRange.singleton(blocker_version))}"
)

for cand, blocker_pkg, pos_range in self.pending_range_blocks:
if cand != normalized:
Expand Down
42 changes: 39 additions & 3 deletions nab-python/tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,9 +1669,9 @@ def test_blocker_reason_outlives_a_later_empty_range(self) -> None:
# Nothing falls in this range, so the second ask has no blockers.
assert provider.choose_version("foo", SpecifierSet(">=5.0").to_range()) is None

assert (
provider.get_no_versions_reason("foo")
== "every version in range was rejected: requires bar != 1.0"
assert provider.get_no_versions_reason("foo") == (
"every version in range was rejected:"
" requires bar in [2.0, 2.0] but solution has it in [1.0, 1.0]"
)

def test_sdist_only_under_dynamic_local_names_build_policy(self) -> None:
Expand Down Expand Up @@ -1832,6 +1832,42 @@ def test_post_release_pin_blocker_renders_post_excluding_bound(self) -> None:
" but solution has it in [2.0.post1, 2.0.post1]" in reason
)

def test_decision_block_rejection_names_the_requirement(self) -> None:
"""The decision-block diagnostic names the candidate's real
requirement, mirroring the range-block path. ``foo`` 1.0 requires
``bar==2.0`` and the resolver has already decided ``bar==1.0``, so
every ``foo`` candidate is rejected. The message must name the
``bar==2.0`` foo needs, not ``requires bar != 1.0`` (which wrongly
implies any bar other than 1.0 would satisfy foo).

The ranges are spelled out rather than interpolated, so the
assertion fails if the message ever prints the debug repr again.
"""
coordinator = make_coordinator(
[make_wheel("1.0")],
metadata_text=(
"Metadata-Version: 2.1\n"
"Name: foo\n"
"Version: 1.0\n"
"Requires-Dist: bar==2.0\n"
),
package="foo",
)
provider = Provider(
coordinator,
target=_PY312,
root_requirements={"foo": VersionRange.full(admit_arbitrary=False)},
)
provider.solution_decisions["bar"] = V("1.0")
result = provider.choose_version("foo", VersionRange.full())
assert result is None
reason = provider.get_no_versions_reason("foo")
assert reason is not None
assert "<VersionRange" not in reason
assert "AFTER_LOCALS" not in reason
assert "requires bar in [2.0, 2.0] but solution has it in [1.0, 1.0]" in reason
assert "requires bar != 1.0" not in reason


def _sdist_entry(version: str) -> dict[str, object]:
"""A Simple-API file record for ``foo``'s sdist at ``version``."""
Expand Down
5 changes: 4 additions & 1 deletion nab-python/tests/test_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -3266,10 +3266,13 @@ def test_constraint_does_not_hide_the_transitive_blocker(
_resolved(pyproject, _FAKE_TRANSPORT, python_version="3.12.0")

diagnostics = str(info.value).split("Diagnostics:")[1]
assert "<VersionRange" not in diagnostics
assert (
"foo: every version in range was rejected: requires lib != 9.0"
"foo: every version in range was rejected:"
" requires lib in [5.0, 5.0] but solution has it in [9.0, 9.0]"
in diagnostics
)
assert "requires lib != 9.0" not in diagnostics
assert "foo: no version matches the requirement" not in diagnostics

def test_blocker_diagnostics_render_readable_ranges(self, tmp_path: Path) -> None:
Expand Down