Skip to content

Commit e3af66e

Browse files
Merge pull request #975 from rd4398/resolver-return-multiple-values
refactor(resolver): return lists of matching versions
2 parents a09f306 + 179c096 commit e3af66e

12 files changed

Lines changed: 354 additions & 315 deletions

docs/customization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ source url can be provided directly in settings.yaml. Optionally the
9090
downloaded sdist can be renamed. Both the url and the destination filename
9191
support templating. The only supported template variable are:
9292

93-
- `version` - it is replaced by the version returned by the `resolve_source`
93+
- `version` - it is replaced by the resolved version of the package
9494
- `canonicalized_name` - it is replaced by the canonicalized name of the
9595
package specified in the requirement, specifically it applies `canonicalize_name(req.nam)`
9696

docs/reference/hooks.rst

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -191,27 +191,13 @@ Source hooks
191191

192192
.. currentmodule:: fromager.sources
193193

194-
.. autofromagerhook:: default_resolve_source
195-
196-
The ``resolve_source()`` function is responsible for resolving a
197-
requirement and acquiring the source for that version of a
198-
package. The default is to use pypi.org to resolve the requirement.
199-
200-
The arguments are the ``WorkContext``, the ``Requirement`` being
201-
evaluated, and the URL to the sdist index.
202-
203-
The return value is ``Tuple[str, Version]`` where the first member is
204-
the url from which the source can be downloaded and the second member
205-
is the version of the resolved package.
206-
207194
.. autofromagerhook:: default_download_source
208195

209196
The ``download_source()`` function is responsible for downloading the
210197
source from a URL.
211198

212199
The arguments are the ``WorkContext``, the ``Requirement`` being
213-
evaluated, version of the package being downloaded, the URL
214-
from which the source can be downloaded as returned by ``resolve_source``,
200+
evaluated, version of the package being downloaded, the download URL,
215201
and the output directory in which the source should be downloaded.
216202

217203
The return value should be a ``pathlib.Path`` file path to the downloaded source.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ get_build_backend_dependencies = "fromager.dependencies:default_get_build_backen
101101
get_build_sdist_dependencies = "fromager.dependencies:default_get_build_sdist_dependencies"
102102
resolver_provider = "fromager.resolver:default_resolver_provider"
103103
download_source = "fromager.sources:default_download_source"
104-
resolve_source = "fromager.sources:default_resolve_source"
105104
build_sdist = "fromager.sources:default_build_sdist"
106105
build_wheel = "fromager.wheels:default_build_wheel"
107106

src/fromager/requirement_resolver.py renamed to src/fromager/bootstrap_requirement_resolver.py

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@
2222
logger = logging.getLogger(__name__)
2323

2424

25-
class RequirementResolver:
26-
"""Resolve package requirements from PyPI or dependency graph.
25+
class BootstrapRequirementResolver:
26+
"""Resolve package requirements from PyPI or dependency graph during bootstrap.
2727
28-
Single Responsibility: Coordinate resolution strategies.
28+
Single Responsibility: Coordinate resolution strategies for bootstrap process.
2929
Reason to Change: Resolution algorithm or provider priorities change.
3030
3131
Resolution strategies (in order):
@@ -51,7 +51,10 @@ def __init__(
5151
self.prev_graph = prev_graph
5252
# Session-level resolution cache to avoid re-resolving same requirements
5353
# Key: (requirement_string, pre_built) to distinguish source vs prebuilt
54-
self._resolved_requirements: dict[tuple[str, bool], tuple[str, Version]] = {}
54+
# Value: list of (url, version) tuples sorted by version (highest first)
55+
self._resolved_requirements: dict[
56+
tuple[str, bool], list[tuple[str, Version]]
57+
] = {}
5558

5659
def resolve(
5760
self,
@@ -60,7 +63,7 @@ def resolve(
6063
parent_req: Requirement | None = None,
6164
pre_built: bool | None = None,
6265
) -> tuple[str, Version]:
63-
"""Resolve package requirement.
66+
"""Resolve package requirement to the best matching version.
6467
6568
Tries resolution strategies in order:
6669
1. Session cache (if previously resolved)
@@ -75,7 +78,7 @@ def resolve(
7578
If None (default), uses package build info to determine.
7679
7780
Returns:
78-
Tuple of (url, resolved_version)
81+
(url, version) tuple for the highest matching version
7982
8083
Raises:
8184
ValueError: If req contains a git URL and pre_built is False
@@ -98,23 +101,22 @@ def resolve(
98101
cached_result = self.get_cached_resolution(req, pre_built)
99102
if cached_result is not None:
100103
logger.debug(f"resolved {req} from cache")
101-
return cached_result
104+
return cached_result[0]
102105

103106
# Resolve using strategies
104-
url, resolved_version = self._resolve(req, req_type, parent_req, pre_built)
107+
results = self._resolve(req, req_type, parent_req, pre_built)
105108

106109
# Cache the result
107-
result = (url, resolved_version)
108-
self.cache_resolution(req, pre_built, result)
109-
return url, resolved_version
110+
self.cache_resolution(req, pre_built, results)
111+
return results[0]
110112

111113
def _resolve(
112114
self,
113115
req: Requirement,
114116
req_type: RequirementType,
115117
parent_req: Requirement | None,
116118
pre_built: bool,
117-
) -> tuple[str, Version]:
119+
) -> list[tuple[str, Version]]:
118120
"""Internal resolution logic without caching.
119121
120122
Tries resolution strategies in order:
@@ -128,7 +130,7 @@ def _resolve(
128130
pre_built: Whether to resolve prebuilt (True) or source (False)
129131
130132
Returns:
131-
Tuple of (url, resolved_version)
133+
List of (url, version) tuples sorted by version (highest first)
132134
"""
133135
# Try graph
134136
cached_resolution = self._resolve_from_graph(
@@ -139,43 +141,48 @@ def _resolve(
139141
)
140142

141143
if cached_resolution and not req.url:
142-
url, resolved_version = cached_resolution
143-
logger.debug(f"resolved from previous bootstrap to {resolved_version}")
144-
return url, resolved_version
144+
logger.debug(
145+
f"resolved from previous bootstrap: {len(cached_resolution)} version(s)"
146+
)
147+
return cached_resolution
145148

146-
# Fallback to PyPI
149+
# Fallback to PyPI using provider pattern
147150
if pre_built:
148151
# Resolve prebuilt wheel
149-
servers = wheels.get_wheel_server_urls(
152+
# Get wheel server URLs (use PyPI as cache/fallback server)
153+
wheel_server_urls = wheels.get_wheel_server_urls(
150154
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
151155
)
152-
url, resolved_version = wheels.resolve_prebuilt_wheel(
153-
ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type
156+
# Use shared retry loop logic from wheels module
157+
return wheels.resolve_all_prebuilt_wheels(
158+
ctx=self.ctx,
159+
req=req,
160+
wheel_server_urls=wheel_server_urls,
161+
req_type=req_type,
154162
)
155163
else:
156164
# Resolve source (sdist)
157-
url, resolved_version = sources.resolve_source(
165+
provider = sources.get_source_provider(
158166
ctx=self.ctx,
159167
req=req,
160168
sdist_server_url=resolver.PYPI_SERVER_URL,
161169
req_type=req_type,
162170
)
163-
164-
return url, resolved_version
171+
return resolver.find_all_matching_from_provider(provider, req)
165172

166173
def get_cached_resolution(
167174
self,
168175
req: Requirement,
169176
pre_built: bool,
170-
) -> tuple[str, Version] | None:
177+
) -> list[tuple[str, Version]] | None:
171178
"""Get a cached resolution result if it exists.
172179
173180
Args:
174181
req: Package requirement to look up in cache
175182
pre_built: Whether looking for prebuilt or source resolution
176183
177184
Returns:
178-
Tuple of (source_url, resolved_version) if cached, None otherwise
185+
List of (url, version) tuples if cached, None otherwise
179186
"""
180187
cache_key = (str(req), pre_built)
181188
return self._resolved_requirements.get(cache_key)
@@ -184,7 +191,7 @@ def cache_resolution(
184191
self,
185192
req: Requirement,
186193
pre_built: bool,
187-
result: tuple[str, Version],
194+
result: list[tuple[str, Version]],
188195
) -> None:
189196
"""Cache a resolution result.
190197
@@ -194,7 +201,7 @@ def cache_resolution(
194201
Args:
195202
req: Package requirement to cache
196203
pre_built: Whether this is a prebuilt or source resolution
197-
result: Tuple of (source_url, resolved_version)
204+
result: List of (url, version) tuples
198205
"""
199206
cache_key = (str(req), pre_built)
200207
self._resolved_requirements[cache_key] = result
@@ -205,7 +212,7 @@ def _resolve_from_graph(
205212
req_type: RequirementType,
206213
pre_built: bool,
207214
parent_req: Requirement | None,
208-
) -> tuple[str, Version] | None:
215+
) -> list[tuple[str, Version]] | None:
209216
"""Resolve from previous dependency graph.
210217
211218
Extracted from Bootstrapper._resolve_from_graph().
@@ -217,7 +224,7 @@ def _resolve_from_graph(
217224
parent_req: Parent requirement for graph traversal
218225
219226
Returns:
220-
Tuple of (url, version) if found in graph, None otherwise
227+
List of (url, version) tuples if found in graph, None otherwise
221228
"""
222229
if not self.prev_graph:
223230
return None
@@ -307,8 +314,8 @@ def _resolve_from_version_source(
307314
self,
308315
version_source: list[tuple[str, Version]],
309316
req: Requirement,
310-
) -> tuple[str, Version] | None:
311-
"""Select best version from candidates.
317+
) -> list[tuple[str, Version]] | None:
318+
"""Filter and return all matching versions from candidates.
312319
313320
Extracted from Bootstrapper._resolve_from_version_source().
314321
@@ -317,7 +324,7 @@ def _resolve_from_version_source(
317324
req: Package requirement with version specifier
318325
319326
Returns:
320-
Tuple of (url, version) for best match, None if no match
327+
List of (url, version) tuples for all matches, None if no matches
321328
"""
322329
if not version_source:
323330
return None
@@ -329,7 +336,8 @@ def _resolve_from_version_source(
329336
constraints=self.ctx.constraints,
330337
use_resolver_cache=False,
331338
)
332-
return resolver.resolve_from_provider(provider, req)
339+
# find_all_matching_from_provider now returns all matching candidates
340+
return resolver.find_all_matching_from_provider(provider, req)
333341
except Exception as err:
334342
logger.debug(f"could not resolve {req} from {version_source}: {err}")
335343
return None

src/fromager/bootstrapper.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@
1919
from packaging.version import Version
2020

2121
from . import (
22+
bootstrap_requirement_resolver,
2223
build_environment,
2324
dependencies,
2425
finders,
2526
hooks,
2627
progress,
27-
requirement_resolver,
2828
resolver,
2929
server,
3030
sources,
@@ -103,8 +103,8 @@ def __init__(
103103
self.test_mode = test_mode
104104
self.why: list[tuple[RequirementType, Requirement, Version]] = []
105105

106-
# Delegate resolution to RequirementResolver
107-
self._resolver = requirement_resolver.RequirementResolver(
106+
# Delegate resolution to BootstrapRequirementResolver
107+
self._resolver = bootstrap_requirement_resolver.BootstrapRequirementResolver(
108108
ctx=ctx,
109109
prev_graph=prev_graph,
110110
)
@@ -176,11 +176,11 @@ def resolve_version(
176176
) -> tuple[str, Version]:
177177
"""Resolve the version of a requirement.
178178
179-
Returns the source URL and the version of the requirement.
179+
Returns the source URL and the version of the requirement (highest matching version).
180180
181181
Git URL resolution stays in Bootstrapper because it requires
182182
build orchestration (BuildEnvironment, build dependencies).
183-
Delegates PyPI/graph resolution to RequirementResolver.
183+
Delegates PyPI/graph resolution to BootstrapRequirementResolver.
184184
"""
185185
if req.url:
186186
if req_type != RequirementType.TOP_LEVEL:
@@ -193,19 +193,22 @@ def resolve_version(
193193
cached_result = self._resolver.get_cached_resolution(req, pre_built=False)
194194
if cached_result is not None:
195195
logger.debug(f"resolved {req} from cache")
196-
return cached_result
196+
# Pick highest version from cached list
197+
return cached_result[0]
197198

198199
logger.info("resolving source via URL, ignoring any plugins")
199200
source_url, resolved_version = self._resolve_version_from_git_url(req=req)
200201
# Cache the git URL resolution (always source, not prebuilt)
202+
# Store as list for consistency with cache structure
201203
self._resolver.cache_resolution(
202-
req, pre_built=False, result=(source_url, resolved_version)
204+
req, pre_built=False, result=[(source_url, resolved_version)]
203205
)
204206
return source_url, resolved_version
205207

206208
# Delegate to RequirementResolver
207209
parent_req = self.why[-1][1] if self.why else None
208210

211+
# Returns the highest matching version
209212
return self._resolver.resolve(
210213
req=req,
211214
req_type=req_type,

src/fromager/commands/list_overrides.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ def list_overrides(
7777
"prebuilt_wheel",
7878
# from overrides.py, found by searching for find_override_method
7979
"download_source",
80-
"resolve_source",
8180
"get_resolver_provider",
8281
"prepare_source",
8382
"build_sdist",

0 commit comments

Comments
 (0)