Skip to content

Commit eb93e88

Browse files
fix(resolver): indicate resolver type in error messages
Add provider description to error messages when resolution fails. Previously, errors from custom resolvers (GitHub, GitLab, etc.) were indistinguishable from PyPI resolver errors, making debugging difficult. Closes: #858 Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 6648ae6 commit eb93e88

File tree

2 files changed

+90
-2
lines changed

2 files changed

+90
-2
lines changed

src/fromager/resolver.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,12 @@ def resolve_from_provider(
163163
result = rslvr.resolve([req])
164164
except resolvelib.resolvers.ResolverException as err:
165165
constraint = provider.constraints.get_constraint(req.name)
166+
provider_desc = provider.get_provider_description()
167+
# Include the original error message to preserve detailed information
168+
# (e.g., file types, pre-release info from PyPIProvider)
169+
original_msg = str(err)
166170
raise resolvelib.resolvers.ResolverException(
167-
f"Unable to resolve requirement specifier {req} with constraint {constraint}"
171+
f"Unable to resolve requirement specifier {req} with constraint {constraint} using {provider_desc}: {original_msg}"
168172
) from err
169173
# resolvelib actually just returns one candidate per requirement.
170174
# result.mapping is map from an identifier to its resolved candidate
@@ -395,6 +399,13 @@ def cache_key(self) -> str:
395399
"""
396400
raise NotImplementedError()
397401

402+
def get_provider_description(self) -> str:
403+
"""Return a human-readable description of the provider type
404+
405+
This is used in error messages to indicate what resolver was being used.
406+
"""
407+
return type(self).__name__
408+
398409
def find_candidates(self, identifier: str) -> Candidates:
399410
"""Find unfiltered candidates"""
400411
raise NotImplementedError()
@@ -552,6 +563,9 @@ def find_matches(
552563
class PyPIProvider(BaseProvider):
553564
"""Lookup package and versions from a simple Python index (PyPI)"""
554565

566+
def get_provider_description(self) -> str:
567+
return f"PyPI resolver (searching at {self.sdist_server_url})"
568+
555569
def __init__(
556570
self,
557571
include_sdists: bool = True,
@@ -646,7 +660,7 @@ def find_matches(
646660
file_type_info = "wheels"
647661

648662
raise resolvelib.resolvers.ResolverException(
649-
f"found no match for {r}, searching for {file_type_info}, {prerelease_info} pre-release versions, in cache or at {self.sdist_server_url}"
663+
f"found no match for {r} using {self.get_provider_description()}, searching for {file_type_info}, {prerelease_info} pre-release versions, in cache or at {self.sdist_server_url}"
650664
)
651665
return sorted(candidates, key=attrgetter("version", "build_tag"), reverse=True)
652666

@@ -707,6 +721,9 @@ def _re_match_function(
707721
logger.debug(f"{identifier}: could not parse version from {value}: {err}")
708722
return None
709723

724+
def get_provider_description(self) -> str:
725+
return "custom resolver (GenericProvider)"
726+
710727
@property
711728
def cache_key(self) -> str:
712729
raise NotImplementedError("GenericProvider does not implement caching")
@@ -757,6 +774,9 @@ def __init__(
757774
self.organization = organization
758775
self.repo = repo
759776

777+
def get_provider_description(self) -> str:
778+
return f"GitHub tag resolver (repository: {self.organization}/{self.repo})"
779+
760780
@property
761781
def cache_key(self) -> str:
762782
return f"{self.organization}/{self.repo}"
@@ -828,6 +848,9 @@ def __init__(
828848
f"{self.server_url}/api/v4/projects/{encoded_path}/repository/tags"
829849
)
830850

851+
def get_provider_description(self) -> str:
852+
return f"GitLab tag resolver (project: {self.server_url}/{self.project_path})"
853+
831854
@property
832855
def cache_key(self) -> str:
833856
return f"{self.server_url}/{self.project_path}"

tests/test_resolver.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,3 +1033,68 @@ def test_pep592_support_constraint_mismatch() -> None:
10331033
def test_extract_filename_from_url(url, filename) -> None:
10341034
result = resolver.extract_filename_from_url(url)
10351035
assert result == filename
1036+
1037+
1038+
def test_custom_resolver_error_message_missing_tag() -> None:
1039+
"""Test that error message indicates custom resolver when tag doesn't exist.
1040+
1041+
This reproduces issue #858 where the error message mentions PyPI and sdists
1042+
even when using a custom resolver like GitHubTagProvider.
1043+
"""
1044+
with requests_mock.Mocker() as r:
1045+
# Mock GitHub API to return empty tags (simulating missing tag)
1046+
r.get(
1047+
"https://api.github.com:443/repos/test-org/test-repo/tags",
1048+
json=[], # Empty tags list - tag doesn't exist
1049+
)
1050+
1051+
provider = resolver.GitHubTagProvider(organization="test-org", repo="test-repo")
1052+
1053+
with pytest.raises(resolvelib.resolvers.ResolverException) as exc_info:
1054+
resolver.resolve_from_provider(provider, Requirement("test-package==1.0.0"))
1055+
1056+
error_message = str(exc_info.value)
1057+
assert (
1058+
"GitHub" in error_message
1059+
or "test-org/test-repo" in error_message
1060+
or "custom resolver" in error_message.lower()
1061+
), (
1062+
f"Error message should indicate custom resolver was used (GitHub tag resolver), "
1063+
f"but got: {error_message}"
1064+
)
1065+
# Should NOT mention PyPI when using GitHub resolver
1066+
assert "pypi.org" not in error_message.lower(), (
1067+
f"Error message incorrectly mentions PyPI when using GitHub resolver: {error_message}"
1068+
)
1069+
1070+
1071+
def test_custom_resolver_error_message_via_resolve() -> None:
1072+
"""Test error message when using resolve() function with custom resolver override."""
1073+
1074+
def custom_resolver_provider(*args, **kwargs):
1075+
"""Custom resolver that returns GitHubTagProvider."""
1076+
return resolver.GitHubTagProvider(organization="test-org", repo="test-repo")
1077+
1078+
with requests_mock.Mocker() as r:
1079+
# Mock GitHub API to return empty tags
1080+
r.get(
1081+
"https://api.github.com:443/repos/test-org/test-repo/tags",
1082+
json=[],
1083+
)
1084+
1085+
provider = custom_resolver_provider()
1086+
1087+
with pytest.raises(resolvelib.resolvers.ResolverException) as exc_info:
1088+
resolver.resolve_from_provider(provider, Requirement("test-package==1.0.0"))
1089+
1090+
error_message = str(exc_info.value)
1091+
# After fix for issue #858, the error message should indicate that a GitHub resolver was used
1092+
assert (
1093+
"GitHub" in error_message
1094+
or "test-org/test-repo" in error_message
1095+
or "custom resolver" in error_message.lower()
1096+
), f"Error message should indicate GitHub resolver was used: {error_message}"
1097+
# Should NOT mention PyPI when using GitHub resolver
1098+
assert "pypi.org" not in error_message.lower(), (
1099+
f"Error message incorrectly mentions PyPI when using GitHub resolver: {error_message}"
1100+
)

0 commit comments

Comments
 (0)