@@ -40,15 +40,20 @@ def __init__(
4040 self ,
4141 ctx : context .WorkContext ,
4242 prev_graph : DependencyGraph | None = None ,
43+ multiple_versions : bool = False ,
4344 ) -> None :
4445 """Initialize requirement resolver.
4546
4647 Args:
4748 ctx: Work context with constraints and settings
4849 prev_graph: Optional previous dependency graph for caching
50+ multiple_versions: If ``True``, age filtering returns an empty
51+ list instead of falling back to all candidates, letting the
52+ caller decide how to handle the case.
4953 """
5054 self .ctx = ctx
5155 self .prev_graph = prev_graph
56+ self .multiple_versions = multiple_versions
5257 # Session-level resolution cache to avoid re-resolving same requirements
5358 # Key: (requirement_string, pre_built) to distinguish source vs prebuilt
5459 # Value: tuple of (url, version) tuples sorted by version (highest first)
@@ -65,6 +70,7 @@ def resolve(
6570 parent_req : Requirement | None = None ,
6671 pre_built : bool | None = None ,
6772 return_all_versions : bool = False ,
73+ skip_age_filter : bool = False ,
6874 ) -> list [tuple [str , Version ]]:
6975 """Resolve package requirement to matching version(s).
7076
@@ -81,6 +87,9 @@ def resolve(
8187 If None (default), uses package build info to determine.
8288 return_all_versions: If True, return all matching versions. If False,
8389 return only the highest matching version.
90+ skip_age_filter: If ``True``, resolve without applying
91+ ``max_release_age`` filtering. Used for fallback resolution
92+ when the initial age-filtered attempt returned no candidates.
8493
8594 Returns:
8695 List of (url, version) tuples sorted by version (highest first).
@@ -104,25 +113,38 @@ def resolve(
104113 f"Git URL requirements must be handled by Bootstrapper: { req } "
105114 )
106115
107- # Check session cache (keyed by requirement + pre_built)
108- cached_result = self .get_cached_resolution (req , pre_built )
109- if cached_result is not None :
110- logger .debug (f"resolved { req } from cache" )
111- return list (cached_result ) if return_all_versions else [cached_result [0 ]]
116+ # Skip session cache when age filter is overridden to avoid
117+ # mixing filtered and unfiltered results.
118+ if not skip_age_filter :
119+ cached_result = self .get_cached_resolution (req , pre_built )
120+ if cached_result is not None :
121+ logger .debug (f"resolved { req } from cache" )
122+ return (
123+ list (cached_result ) if return_all_versions else [cached_result [0 ]]
124+ )
112125
113126 # Resolve using strategies
114- results = self ._resolve (req , req_type , parent_req , pre_built )
127+ results = self ._resolve (
128+ req , req_type , parent_req , pre_built , skip_age_filter = skip_age_filter
129+ )
115130
116- # Cache the result
117- self .cache_resolution (req , pre_built , results )
118- return results if return_all_versions else [results [0 ]]
131+ # Cache non-empty results from normal (age-filtered) resolution only.
132+ if results and not skip_age_filter :
133+ self .cache_resolution (req , pre_built , results )
134+
135+ if return_all_versions :
136+ return results
137+ if not results :
138+ raise RuntimeError (f"No versions found for { req } " )
139+ return [results [0 ]]
119140
120141 def _resolve (
121142 self ,
122143 req : Requirement ,
123144 req_type : RequirementType ,
124145 parent_req : Requirement | None ,
125146 pre_built : bool ,
147+ skip_age_filter : bool = False ,
126148 ) -> list [tuple [str , Version ]]:
127149 """Internal resolution logic without caching.
128150
@@ -135,6 +157,7 @@ def _resolve(
135157 req_type: Type of requirement
136158 parent_req: Parent requirement from dependency chain
137159 pre_built: Whether to resolve prebuilt (True) or source (False)
160+ skip_age_filter: If ``True``, skip ``max_release_age`` filtering.
138161
139162 Returns:
140163 List of (url, version) tuples sorted by version (highest first)
@@ -175,9 +198,14 @@ def _resolve(
175198 sdist_server_url = resolver .PYPI_SERVER_URL ,
176199 req_type = req_type ,
177200 )
178- max_age_cutoff = resolver ._compute_max_age_cutoff (self .ctx )
201+ max_age_cutoff = (
202+ None if skip_age_filter else resolver ._compute_max_age_cutoff (self .ctx )
203+ )
179204 return resolver .find_all_matching_from_provider (
180- provider , req , max_age_cutoff = max_age_cutoff
205+ provider ,
206+ req ,
207+ max_age_cutoff = max_age_cutoff ,
208+ fallback_on_empty_age_filter = not self .multiple_versions ,
181209 )
182210
183211 def get_cached_resolution (
0 commit comments