@@ -165,8 +165,12 @@ def resolve_from_provider(
165165 result = rslvr .resolve ([req ])
166166 except resolvelib .resolvers .ResolverException as err :
167167 constraint = provider .constraints .get_constraint (req .name )
168+ provider_desc = provider .get_provider_description ()
169+ # Include the original error message to preserve detailed information
170+ # (e.g., file types, pre-release info from PyPIProvider)
171+ original_msg = str (err )
168172 raise resolvelib .resolvers .ResolverException (
169- f"Unable to resolve requirement specifier { req } with constraint { constraint } "
173+ f"Unable to resolve requirement specifier { req } with constraint { constraint } using { provider_desc } : { original_msg } "
170174 ) from err
171175 # resolvelib actually just returns one candidate per requirement.
172176 # result.mapping is map from an identifier to its resolved candidate
@@ -380,6 +384,7 @@ def get_project_from_pypi(
380384
381385class BaseProvider (ExtrasProvider ):
382386 resolver_cache : typing .ClassVar [ResolverCache ] = {}
387+ provider_description : typing .ClassVar [str ]
383388
384389 def __init__ (
385390 self ,
@@ -402,6 +407,16 @@ def cache_key(self) -> str:
402407 """
403408 raise NotImplementedError ()
404409
410+ def get_provider_description (self ) -> str :
411+ """Return a human-readable description of the provider type
412+
413+ This is used in error messages to indicate what resolver was being used.
414+ The ClassVar `provider_description` must be set by each subclass.
415+ If it contains format placeholders like {self.attr}, it will be formatted
416+ with the instance. Strings without placeholders are returned unchanged.
417+ """
418+ return self .provider_description .format (self = self )
419+
405420 def find_candidates (self , identifier : str ) -> Candidates :
406421 """Find unfiltered candidates"""
407422 raise NotImplementedError ()
@@ -512,6 +527,7 @@ def _get_cached_candidates(self, identifier: str) -> list[Candidate]:
512527
513528 def _find_cached_candidates (self , identifier : str ) -> Candidates :
514529 """Find candidates with caching"""
530+ cached_candidates : list [Candidate ] = []
515531 if self .use_cache_candidates :
516532 cached_candidates = self ._get_cached_candidates (identifier )
517533 if cached_candidates :
@@ -538,6 +554,16 @@ def _find_cached_candidates(self, identifier: str) -> Candidates:
538554 )
539555 return candidates
540556
557+ def _get_no_match_error_message (
558+ self , identifier : str , requirements : RequirementsMap
559+ ) -> str :
560+ """Generate an error message when no candidates are found.
561+
562+ Subclasses should override this to provide provider-specific error details.
563+ """
564+ r = next (iter (requirements [identifier ]))
565+ return f"found no match for { r } using { self .get_provider_description ()} "
566+
541567 def find_matches (
542568 self ,
543569 identifier : str ,
@@ -553,12 +579,20 @@ def find_matches(
553579 identifier , requirements , incompatibilities , candidate
554580 )
555581 ]
582+ if not candidates :
583+ raise resolvelib .resolvers .ResolverException (
584+ self ._get_no_match_error_message (identifier , requirements )
585+ )
556586 return sorted (candidates , key = attrgetter ("version" , "build_tag" ), reverse = True )
557587
558588
559589class PyPIProvider (BaseProvider ):
560590 """Lookup package and versions from a simple Python index (PyPI)"""
561591
592+ provider_description : typing .ClassVar [str ] = (
593+ "PyPI resolver (searching at {self.sdist_server_url})"
594+ )
595+
562596 def __init__ (
563597 self ,
564598 include_sdists : bool = True ,
@@ -623,39 +657,39 @@ def validate_candidate(
623657 return False
624658 return True
625659
660+ def _get_no_match_error_message (
661+ self , identifier : str , requirements : RequirementsMap
662+ ) -> str :
663+ """Generate a PyPI-specific error message with file type and pre-release details."""
664+ r = next (iter (requirements [identifier ]))
665+
666+ # Determine if pre-releases are allowed
667+ req_allows_prerelease = bool (r .specifier ) and bool (r .specifier .prereleases )
668+ allow_prerelease = (
669+ self .constraints .allow_prerelease (r .name ) or req_allows_prerelease
670+ )
671+ prerelease_info = "including" if allow_prerelease else "ignoring"
672+
673+ # Determine the file type that was allowed
674+ if self .include_sdists and self .include_wheels :
675+ file_type_info = "any file type"
676+ elif self .include_sdists :
677+ file_type_info = "sdists"
678+ else :
679+ file_type_info = "wheels"
680+
681+ return (
682+ f"found no match for { r } using { self .get_provider_description ()} , "
683+ f"searching for { file_type_info } , { prerelease_info } pre-release versions"
684+ )
685+
626686 def find_matches (
627687 self ,
628688 identifier : str ,
629689 requirements : RequirementsMap ,
630690 incompatibilities : CandidatesMap ,
631691 ) -> Candidates :
632- candidates = super ().find_matches (identifier , requirements , incompatibilities )
633- if not candidates :
634- # Try to construct a meaningful error message that points out the
635- # type(s) of files the resolver has been told it can choose as a
636- # hint in case that should be adjusted for the package that does not
637- # resolve.
638- r = next (iter (requirements [identifier ]))
639-
640- # Determine if pre-releases are allowed
641- req_allows_prerelease = bool (r .specifier ) and bool (r .specifier .prereleases )
642- allow_prerelease = (
643- self .constraints .allow_prerelease (r .name ) or req_allows_prerelease
644- )
645- prerelease_info = "including" if allow_prerelease else "ignoring"
646-
647- # Determine the file type that was allowed
648- if self .include_sdists and self .include_wheels :
649- file_type_info = "any file type"
650- elif self .include_sdists :
651- file_type_info = "sdists"
652- else :
653- file_type_info = "wheels"
654-
655- raise resolvelib .resolvers .ResolverException (
656- f"found no match for { r } , searching for { file_type_info } , { prerelease_info } pre-release versions, in cache or at { self .sdist_server_url } "
657- )
658- return sorted (candidates , key = attrgetter ("version" , "build_tag" ), reverse = True )
692+ return super ().find_matches (identifier , requirements , incompatibilities )
659693
660694
661695class MatchFunction (typing .Protocol ):
@@ -714,6 +748,8 @@ def _re_match_function(
714748 logger .debug (f"{ identifier } : could not parse version from { value } : { err } " )
715749 return None
716750
751+ provider_description : typing .ClassVar [str ] = "custom resolver (GenericProvider)"
752+
717753 @property
718754 def cache_key (self ) -> str :
719755 raise NotImplementedError ("GenericProvider does not implement caching" )
@@ -752,6 +788,9 @@ class GitHubTagProvider(GenericProvider):
752788 Assumes that upstream uses version tags `1.2.3` or `v1.2.3`.
753789 """
754790
791+ provider_description : typing .ClassVar [str ] = (
792+ "GitHub tag resolver (repository: {self.organization}/{self.repo})"
793+ )
755794 host = "github.com:443"
756795 api_url = "https://api.{self.host}/repos/{self.organization}/{self.repo}/tags"
757796
@@ -828,6 +867,10 @@ def _find_tags(
828867class GitLabTagProvider (GenericProvider ):
829868 """Lookup tarball and version from GitLab git tags"""
830869
870+ provider_description : typing .ClassVar [str ] = (
871+ "GitLab tag resolver (project: {self.server_url}/{self.project_path})"
872+ )
873+
831874 def __init__ (
832875 self ,
833876 project_path : str ,
0 commit comments