Skip to content

Commit 13a0ff5

Browse files
rd4398claude
andcommitted
refactor(resolver): inline _resolve() into resolve() and clarify fallback logic
Inline `_resolve()` into `resolve()` to remove unnecessary indirection. Restructure the empty-results handling as explicit early returns with per-case log messages. Guard against IndexError on empty results. Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Rohan Devasthale <rdevasth@redhat.com>
1 parent a33acb9 commit 13a0ff5

3 files changed

Lines changed: 112 additions & 128 deletions

File tree

src/fromager/bootstrap_requirement_resolver.py

Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -121,42 +121,7 @@ def resolve(
121121
logger.debug(f"resolved {req} from cache")
122122
return list(cached_result) if return_all_versions else [cached_result[0]]
123123

124-
# Resolve using strategies
125-
results = self._resolve(req, req_type, parent_req, pre_built)
126-
127-
# Only cache non-empty results.
128-
if results:
129-
self.cache_resolution(req, pre_built, results)
130-
131-
if not results:
132-
return []
133-
return results if return_all_versions else [results[0]]
134-
135-
def _resolve(
136-
self,
137-
req: Requirement,
138-
req_type: RequirementType,
139-
parent_req: Requirement | None,
140-
pre_built: bool,
141-
) -> list[tuple[str, Version]]:
142-
"""Internal resolution logic without caching.
143-
144-
Tries resolution strategies in order:
145-
1. Previous dependency graph
146-
2. PyPI resolution (source or prebuilt)
147-
3. Remote wheel cache server (multi-version mode only, when age
148-
filtering produced no source candidates)
149-
150-
Args:
151-
req: Package requirement
152-
req_type: Type of requirement
153-
parent_req: Parent requirement from dependency chain
154-
pre_built: Whether to resolve prebuilt (True) or source (False)
155-
156-
Returns:
157-
List of (url, version) tuples sorted by version (highest first)
158-
"""
159-
# Try graph
124+
# Try previous dependency graph
160125
cached_resolution = self._resolve_from_graph(
161126
req=req,
162127
req_type=req_type,
@@ -168,49 +133,63 @@ def _resolve(
168133
logger.debug(
169134
f"resolved from previous bootstrap: {len(cached_resolution)} version(s)"
170135
)
171-
return cached_resolution
172-
173-
# Fallback to PyPI using provider pattern
174-
if pre_built:
136+
results = cached_resolution
137+
elif pre_built:
175138
# Resolve prebuilt wheel
176-
# Get wheel server URLs (use PyPI as cache/fallback server)
177139
wheel_server_urls = wheels.get_wheel_server_urls(
178140
self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL
179141
)
180-
# Use shared retry loop logic from wheels module
181-
return wheels.resolve_all_prebuilt_wheels(
142+
results = wheels.resolve_all_prebuilt_wheels(
182143
ctx=self.ctx,
183144
req=req,
184145
wheel_server_urls=wheel_server_urls,
185146
req_type=req_type,
186147
)
148+
else:
149+
# Resolve source (sdist)
150+
provider = sources.get_source_provider(
151+
ctx=self.ctx,
152+
req=req,
153+
sdist_server_url=resolver.PYPI_SERVER_URL,
154+
req_type=req_type,
155+
)
156+
max_age_cutoff = resolver._compute_max_age_cutoff(self.ctx)
157+
results = resolver.find_all_matching_from_provider(
158+
provider,
159+
req,
160+
max_age_cutoff=max_age_cutoff,
161+
fallback_on_empty_age_filter=not self.multiple_versions,
162+
)
187163

188-
# Resolve source (sdist)
189-
provider = sources.get_source_provider(
190-
ctx=self.ctx,
191-
req=req,
192-
sdist_server_url=resolver.PYPI_SERVER_URL,
193-
req_type=req_type,
194-
)
195-
max_age_cutoff = resolver._compute_max_age_cutoff(self.ctx)
196-
results = resolver.find_all_matching_from_provider(
197-
provider,
198-
req,
199-
max_age_cutoff=max_age_cutoff,
200-
fallback_on_empty_age_filter=not self.multiple_versions,
201-
)
164+
if results:
165+
pass
166+
elif not self.multiple_versions:
167+
logger.warning(
168+
"%s: resolver returned no results",
169+
req.name,
170+
)
171+
elif not self.cache_wheel_server_url:
172+
logger.warning(
173+
"%s: resolver returned no results and we have no "
174+
"wheel cache server URL to fall back on",
175+
req.name,
176+
)
177+
else:
178+
logger.info(
179+
"%s: no results found with normal resolution, "
180+
"falling back to the cache server %s",
181+
req.name,
182+
self.cache_wheel_server_url,
183+
)
184+
results = self._resolve_from_cache_server(req)
202185

203-
if not results and self.multiple_versions and self.cache_wheel_server_url:
204-
results = self._resolve_from_cache_server(req)
186+
# Only cache non-empty results.
187+
if results:
188+
self.cache_resolution(req, pre_built, results)
205189

206190
if not results:
207-
logger.warning(
208-
"%s: could not find any versions (all filtered by "
209-
"max-release-age and no cached wheel on server)",
210-
req.name,
211-
)
212-
213-
return results
191+
return []
192+
return results if return_all_versions else [results[0]]
214193

215194
def _resolve_from_cache_server(self, req: Requirement) -> list[tuple[str, Version]]:
216195
"""Fall back to the remote wheel cache server for a cached version.

tests/test_bootstrap_requirement_resolver.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ def test_returns_empty_on_exception(self, tmp_context: WorkContext) -> None:
762762
def test_resolve_uses_cache_fallback_when_age_filter_empties(
763763
self, tmp_context: WorkContext
764764
) -> None:
765-
"""_resolve falls back to cache server when age filter produces empty result."""
765+
"""resolve() falls back to cache server when age filter produces empty result."""
766766
brr = BootstrapRequirementResolver(
767767
tmp_context,
768768
multiple_versions=True,
@@ -786,11 +786,12 @@ def test_resolve_uses_cache_fallback_when_age_filter_empties(
786786
return_value=[("http://cache.test/testpkg-1.0.whl", Version("1.0"))],
787787
) as mock_cache,
788788
):
789-
result = brr._resolve(
789+
result = brr.resolve(
790790
req,
791791
RequirementType.INSTALL,
792792
parent_req=None,
793793
pre_built=False,
794+
return_all_versions=True,
794795
)
795796

796797
mock_pypi.assert_called_once()
@@ -801,7 +802,7 @@ def test_resolve_uses_cache_fallback_when_age_filter_empties(
801802
def test_resolve_skips_cache_fallback_in_single_version_mode(
802803
self, tmp_context: WorkContext
803804
) -> None:
804-
"""_resolve does not fall back to cache server in single-version mode."""
805+
"""resolve() does not fall back to cache server in single-version mode."""
805806
brr = BootstrapRequirementResolver(
806807
tmp_context,
807808
multiple_versions=False,
@@ -821,7 +822,7 @@ def test_resolve_skips_cache_fallback_in_single_version_mode(
821822
),
822823
patch.object(brr, "_resolve_from_cache_server") as mock_cache,
823824
):
824-
brr._resolve(
825+
brr.resolve(
825826
req,
826827
RequirementType.INSTALL,
827828
parent_req=None,
@@ -833,7 +834,7 @@ def test_resolve_skips_cache_fallback_in_single_version_mode(
833834
def test_resolve_skips_cache_fallback_when_no_server_url(
834835
self, tmp_context: WorkContext
835836
) -> None:
836-
"""_resolve does not fall back when no cache_wheel_server_url is set."""
837+
"""resolve() does not fall back when no cache_wheel_server_url is set."""
837838
brr = BootstrapRequirementResolver(
838839
tmp_context,
839840
multiple_versions=True,
@@ -853,11 +854,12 @@ def test_resolve_skips_cache_fallback_when_no_server_url(
853854
),
854855
patch.object(brr, "_resolve_from_cache_server") as mock_cache,
855856
):
856-
brr._resolve(
857+
brr.resolve(
857858
req,
858859
RequirementType.INSTALL,
859860
parent_req=None,
860861
pre_built=False,
862+
return_all_versions=True,
861863
)
862864

863865
mock_cache.assert_not_called()

tests/test_bootstrap_requirement_resolver_multiple.py

Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,83 +30,93 @@ def tmp_context(tmp_path: Path) -> WorkContext:
3030
pbi.resolver_ignore_platform = False
3131
pbi.resolver_sdist_server_url.return_value = "https://pypi.org/simple/"
3232
ctx.package_build_info.return_value = pbi
33+
ctx.max_release_age = None
3334
return ctx
3435

3536

37+
_MOCK_VERSIONS = [
38+
("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0")),
39+
("https://pypi.org/testpkg-1.5.tar.gz", Version("1.5")),
40+
("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0")),
41+
]
42+
43+
3644
def test_resolve_return_all_versions_true(tmp_context: WorkContext) -> None:
3745
"""resolve() with return_all_versions=True returns all matching versions."""
38-
resolver = BootstrapRequirementResolver(tmp_context)
39-
40-
# Mock the _resolve method to return multiple versions
41-
with patch.object(
42-
resolver,
43-
"_resolve",
44-
return_value=[
45-
("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0")),
46-
("https://pypi.org/testpkg-1.5.tar.gz", Version("1.5")),
47-
("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0")),
48-
],
46+
brr = BootstrapRequirementResolver(tmp_context)
47+
48+
with (
49+
patch.object(brr, "_resolve_from_graph", return_value=None),
50+
patch(
51+
"fromager.bootstrap_requirement_resolver.sources.get_source_provider",
52+
),
53+
patch(
54+
"fromager.bootstrap_requirement_resolver.resolver"
55+
".find_all_matching_from_provider",
56+
return_value=list(_MOCK_VERSIONS),
57+
),
4958
):
5059
req = Requirement("testpkg>=1.0")
51-
results = resolver.resolve(
60+
results = brr.resolve(
5261
req=req,
5362
req_type=RequirementType.INSTALL,
5463
parent_req=None,
5564
return_all_versions=True,
5665
)
5766

58-
# Should return all 3 versions
5967
assert len(results) == 3
60-
assert results[0] == ("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0"))
61-
assert results[1] == ("https://pypi.org/testpkg-1.5.tar.gz", Version("1.5"))
62-
assert results[2] == ("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0"))
68+
assert results[0] == _MOCK_VERSIONS[0]
69+
assert results[1] == _MOCK_VERSIONS[1]
70+
assert results[2] == _MOCK_VERSIONS[2]
6371

6472

6573
def test_resolve_return_all_versions_false_default(tmp_context: WorkContext) -> None:
6674
"""resolve() with return_all_versions=False (default) returns list with only highest version."""
67-
resolver = BootstrapRequirementResolver(tmp_context)
68-
69-
# Mock the _resolve method to return multiple versions
70-
with patch.object(
71-
resolver,
72-
"_resolve",
73-
return_value=[
74-
("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0")),
75-
("https://pypi.org/testpkg-1.5.tar.gz", Version("1.5")),
76-
("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0")),
77-
],
75+
brr = BootstrapRequirementResolver(tmp_context)
76+
77+
with (
78+
patch.object(brr, "_resolve_from_graph", return_value=None),
79+
patch(
80+
"fromager.bootstrap_requirement_resolver.sources.get_source_provider",
81+
),
82+
patch(
83+
"fromager.bootstrap_requirement_resolver.resolver"
84+
".find_all_matching_from_provider",
85+
return_value=list(_MOCK_VERSIONS),
86+
),
7887
):
7988
req = Requirement("testpkg>=1.0")
80-
81-
# Call without return_all_versions (default False)
82-
results = resolver.resolve(
89+
results = brr.resolve(
8390
req=req,
8491
req_type=RequirementType.INSTALL,
8592
parent_req=None,
8693
)
8794

88-
# Should return list with only the highest version
8995
assert len(results) == 1
90-
assert results[0] == ("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0"))
96+
assert results[0] == _MOCK_VERSIONS[0]
9197

9298

9399
def test_resolve_return_all_versions_uses_cache(tmp_context: WorkContext) -> None:
94100
"""resolve() with return_all_versions=True uses cache correctly."""
95-
resolver = BootstrapRequirementResolver(tmp_context)
96-
97-
# First call - will populate cache
98-
with patch.object(
99-
resolver,
100-
"_resolve",
101-
return_value=[
102-
("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0")),
103-
("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0")),
104-
],
105-
) as mock_resolve:
101+
brr = BootstrapRequirementResolver(tmp_context)
102+
103+
with (
104+
patch.object(brr, "_resolve_from_graph", return_value=None),
105+
patch(
106+
"fromager.bootstrap_requirement_resolver.sources.get_source_provider",
107+
),
108+
patch(
109+
"fromager.bootstrap_requirement_resolver.resolver"
110+
".find_all_matching_from_provider",
111+
return_value=[
112+
("https://pypi.org/testpkg-2.0.tar.gz", Version("2.0")),
113+
("https://pypi.org/testpkg-1.0.tar.gz", Version("1.0")),
114+
],
115+
) as mock_resolve,
116+
):
106117
req = Requirement("testpkg>=1.0")
107118

108-
# First call
109-
results1 = resolver.resolve(
119+
results1 = brr.resolve(
110120
req=req,
111121
req_type=RequirementType.INSTALL,
112122
parent_req=None,
@@ -115,25 +125,22 @@ def test_resolve_return_all_versions_uses_cache(tmp_context: WorkContext) -> Non
115125
assert len(results1) == 2
116126
assert mock_resolve.call_count == 1
117127

118-
# Second call - should use cache
119-
results2 = resolver.resolve(
128+
results2 = brr.resolve(
120129
req=req,
121130
req_type=RequirementType.INSTALL,
122131
parent_req=None,
123132
return_all_versions=True,
124133
)
125134

126-
# Should not call _resolve again
135+
# Should not call resolution again — cache hit
127136
assert mock_resolve.call_count == 1
128-
# Should return same results
129137
assert results2 == results1
130138

131139

132140
def test_resolve_return_all_versions_with_previous_graph(
133141
tmp_context: WorkContext,
134142
) -> None:
135143
"""resolve() with return_all_versions=True works with previous graph."""
136-
# Create graph with multiple versions of the same package
137144
prev_graph = DependencyGraph()
138145
prev_graph.add_dependency(
139146
parent_name=None,
@@ -150,23 +157,19 @@ def test_resolve_return_all_versions_with_previous_graph(
150157
req_version=Version("1.0"),
151158
)
152159

153-
# Mock dependency_graph in context
154160
tmp_context.dependency_graph = prev_graph
155161

156-
resolver = BootstrapRequirementResolver(tmp_context, prev_graph)
162+
brr = BootstrapRequirementResolver(tmp_context, prev_graph)
157163

158-
# Request with version spec that matches both
159164
req = Requirement("testpkg>=1.0")
160-
results = resolver.resolve(
165+
results = brr.resolve(
161166
req=req,
162167
req_type=RequirementType.TOP_LEVEL,
163168
parent_req=None,
164169
return_all_versions=True,
165170
)
166171

167-
# Should return both versions from graph
168172
assert len(results) == 2
169-
# Verify versions (should be sorted highest first)
170173
versions = [v for _, v in results]
171174
assert Version("2.0") in versions
172175
assert Version("1.0") in versions

0 commit comments

Comments
 (0)