@@ -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
@@ -373,6 +377,7 @@ def get_project_from_pypi(
373377
374378class BaseProvider (ExtrasProvider ):
375379 resolver_cache : typing .ClassVar [ResolverCache ] = {}
380+ provider_description : typing .ClassVar [str ]
376381
377382 def __init__ (
378383 self ,
@@ -395,6 +400,20 @@ def cache_key(self) -> str:
395400 """
396401 raise NotImplementedError ()
397402
403+ def get_provider_description (self ) -> str :
404+ """Return a human-readable description of the provider type
405+
406+ This is used in error messages to indicate what resolver was being used.
407+ The ClassVar `provider_description` must be set by each subclass.
408+ If it contains format placeholders like {self.attr}, it will be formatted
409+ with the instance.
410+ """
411+ try :
412+ return self .provider_description .format (self = self )
413+ except (KeyError , AttributeError ):
414+ # No format placeholders or invalid format, return as-is
415+ return self .provider_description
416+
398417 def find_candidates (self , identifier : str ) -> Candidates :
399418 """Find unfiltered candidates"""
400419 raise NotImplementedError ()
@@ -505,6 +524,7 @@ def _get_cached_candidates(self, identifier: str) -> list[Candidate]:
505524
506525 def _find_cached_candidates (self , identifier : str ) -> Candidates :
507526 """Find candidates with caching"""
527+ cached_candidates : list [Candidate ] = []
508528 if self .use_cache_candidates :
509529 cached_candidates = self ._get_cached_candidates (identifier )
510530 if cached_candidates :
@@ -531,6 +551,16 @@ def _find_cached_candidates(self, identifier: str) -> Candidates:
531551 )
532552 return candidates
533553
554+ def _get_no_match_error_message (
555+ self , identifier : str , requirements : RequirementsMap
556+ ) -> str :
557+ """Generate an error message when no candidates are found.
558+
559+ Subclasses should override this to provide provider-specific error details.
560+ """
561+ r = next (iter (requirements [identifier ]))
562+ return f"found no match for { r } using { self .get_provider_description ()} "
563+
534564 def find_matches (
535565 self ,
536566 identifier : str ,
@@ -546,12 +576,18 @@ def find_matches(
546576 identifier , requirements , incompatibilities , candidate
547577 )
548578 ]
579+ if not candidates :
580+ raise resolvelib .resolvers .ResolverException (
581+ self ._get_no_match_error_message (identifier , requirements )
582+ )
549583 return sorted (candidates , key = attrgetter ("version" , "build_tag" ), reverse = True )
550584
551585
552586class PyPIProvider (BaseProvider ):
553587 """Lookup package and versions from a simple Python index (PyPI)"""
554588
589+ provider_description : typing .ClassVar [str ] = "PyPI resolver (searching at {self.sdist_server_url})"
590+
555591 def __init__ (
556592 self ,
557593 include_sdists : bool = True ,
@@ -616,39 +652,39 @@ def validate_candidate(
616652 return False
617653 return True
618654
655+ def _get_no_match_error_message (
656+ self , identifier : str , requirements : RequirementsMap
657+ ) -> str :
658+ """Generate a PyPI-specific error message with file type and pre-release details."""
659+ r = next (iter (requirements [identifier ]))
660+
661+ # Determine if pre-releases are allowed
662+ req_allows_prerelease = bool (r .specifier ) and bool (r .specifier .prereleases )
663+ allow_prerelease = (
664+ self .constraints .allow_prerelease (r .name ) or req_allows_prerelease
665+ )
666+ prerelease_info = "including" if allow_prerelease else "ignoring"
667+
668+ # Determine the file type that was allowed
669+ if self .include_sdists and self .include_wheels :
670+ file_type_info = "any file type"
671+ elif self .include_sdists :
672+ file_type_info = "sdists"
673+ else :
674+ file_type_info = "wheels"
675+
676+ return (
677+ f"found no match for { r } using { self .get_provider_description ()} , "
678+ f"searching for { file_type_info } , { prerelease_info } pre-release versions"
679+ )
680+
619681 def find_matches (
620682 self ,
621683 identifier : str ,
622684 requirements : RequirementsMap ,
623685 incompatibilities : CandidatesMap ,
624686 ) -> Candidates :
625- candidates = super ().find_matches (identifier , requirements , incompatibilities )
626- if not candidates :
627- # Try to construct a meaningful error message that points out the
628- # type(s) of files the resolver has been told it can choose as a
629- # hint in case that should be adjusted for the package that does not
630- # resolve.
631- r = next (iter (requirements [identifier ]))
632-
633- # Determine if pre-releases are allowed
634- req_allows_prerelease = bool (r .specifier ) and bool (r .specifier .prereleases )
635- allow_prerelease = (
636- self .constraints .allow_prerelease (r .name ) or req_allows_prerelease
637- )
638- prerelease_info = "including" if allow_prerelease else "ignoring"
639-
640- # Determine the file type that was allowed
641- if self .include_sdists and self .include_wheels :
642- file_type_info = "any file type"
643- elif self .include_sdists :
644- file_type_info = "sdists"
645- else :
646- file_type_info = "wheels"
647-
648- 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 } "
650- )
651- return sorted (candidates , key = attrgetter ("version" , "build_tag" ), reverse = True )
687+ return super ().find_matches (identifier , requirements , incompatibilities )
652688
653689
654690class MatchFunction (typing .Protocol ):
@@ -707,6 +743,8 @@ def _re_match_function(
707743 logger .debug (f"{ identifier } : could not parse version from { value } : { err } " )
708744 return None
709745
746+ provider_description : typing .ClassVar [str ] = "custom resolver (GenericProvider)"
747+
710748 @property
711749 def cache_key (self ) -> str :
712750 raise NotImplementedError ("GenericProvider does not implement caching" )
@@ -734,6 +772,7 @@ class GitHubTagProvider(GenericProvider):
734772 Assumes that upstream uses version tags `1.2.3` or `v1.2.3`.
735773 """
736774
775+ provider_description : typing .ClassVar [str ] = "GitHub tag resolver (repository: {self.organization}/{self.repo})"
737776 host = "github.com:443"
738777 api_url = "https://api.{self.host}/repos/{self.organization}/{self.repo}/tags"
739778
@@ -799,6 +838,8 @@ def _find_tags(
799838class GitLabTagProvider (GenericProvider ):
800839 """Lookup tarball and version from GitLab git tags"""
801840
841+ provider_description : typing .ClassVar [str ] = "GitLab tag resolver (project: {self.server_url}/{self.project_path})"
842+
802843 def __init__ (
803844 self ,
804845 project_path : str ,
0 commit comments