@@ -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,20 @@ 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 ] = (
590+ "PyPI resolver (searching at {self.sdist_server_url})"
591+ )
592+
555593 def __init__ (
556594 self ,
557595 include_sdists : bool = True ,
@@ -616,39 +654,39 @@ def validate_candidate(
616654 return False
617655 return True
618656
657+ def _get_no_match_error_message (
658+ self , identifier : str , requirements : RequirementsMap
659+ ) -> str :
660+ """Generate a PyPI-specific error message with file type and pre-release details."""
661+ r = next (iter (requirements [identifier ]))
662+
663+ # Determine if pre-releases are allowed
664+ req_allows_prerelease = bool (r .specifier ) and bool (r .specifier .prereleases )
665+ allow_prerelease = (
666+ self .constraints .allow_prerelease (r .name ) or req_allows_prerelease
667+ )
668+ prerelease_info = "including" if allow_prerelease else "ignoring"
669+
670+ # Determine the file type that was allowed
671+ if self .include_sdists and self .include_wheels :
672+ file_type_info = "any file type"
673+ elif self .include_sdists :
674+ file_type_info = "sdists"
675+ else :
676+ file_type_info = "wheels"
677+
678+ return (
679+ f"found no match for { r } using { self .get_provider_description ()} , "
680+ f"searching for { file_type_info } , { prerelease_info } pre-release versions"
681+ )
682+
619683 def find_matches (
620684 self ,
621685 identifier : str ,
622686 requirements : RequirementsMap ,
623687 incompatibilities : CandidatesMap ,
624688 ) -> 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 )
689+ return super ().find_matches (identifier , requirements , incompatibilities )
652690
653691
654692class MatchFunction (typing .Protocol ):
@@ -707,6 +745,8 @@ def _re_match_function(
707745 logger .debug (f"{ identifier } : could not parse version from { value } : { err } " )
708746 return None
709747
748+ provider_description : typing .ClassVar [str ] = "custom resolver (GenericProvider)"
749+
710750 @property
711751 def cache_key (self ) -> str :
712752 raise NotImplementedError ("GenericProvider does not implement caching" )
@@ -734,6 +774,9 @@ class GitHubTagProvider(GenericProvider):
734774 Assumes that upstream uses version tags `1.2.3` or `v1.2.3`.
735775 """
736776
777+ provider_description : typing .ClassVar [str ] = (
778+ "GitHub tag resolver (repository: {self.organization}/{self.repo})"
779+ )
737780 host = "github.com:443"
738781 api_url = "https://api.{self.host}/repos/{self.organization}/{self.repo}/tags"
739782
@@ -799,6 +842,10 @@ def _find_tags(
799842class GitLabTagProvider (GenericProvider ):
800843 """Lookup tarball and version from GitLab git tags"""
801844
845+ provider_description : typing .ClassVar [str ] = (
846+ "GitLab tag resolver (project: {self.server_url}/{self.project_path})"
847+ )
848+
802849 def __init__ (
803850 self ,
804851 project_path : str ,
0 commit comments