Skip to content

Commit fce133c

Browse files
committed
improve error messages from resolver
Add better logging and exceptions in the resolver code to convey more information about what is happening when no match is found. Log info messages for cases like ignoring all candidate files or having no candidates at all. Use standard ResolverException error types and use messages specific to the different cases for searching for different types of files (sdists or wheels). Move all of the logic for checking version compatibility into is_satisified_by() method of the base class for more consistent logging via different paths.
1 parent 0ed6e75 commit fce133c

2 files changed

Lines changed: 54 additions & 29 deletions

File tree

src/fromager/resolver.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ def resolve_from_provider(
123123
rslvr: resolvelib.Resolver = resolvelib.Resolver(provider, reporter)
124124
try:
125125
result = rslvr.resolve([req])
126-
except resolvelib.resolvers.exceptions.ResolutionImpossible as err:
126+
except resolvelib.resolvers.exceptions.ResolverException as err:
127127
constraint = provider.constraints.get_constraint(req.name)
128-
raise ValueError(
128+
raise resolvelib.resolvers.exceptions.ResolverException(
129129
f"Unable to resolve requirement specifier {req} with constraint {constraint}"
130130
) from err
131131
# resolvelib actually just returns one candidate per requirement.
@@ -142,6 +142,8 @@ def get_project_from_pypi(
142142
sdist_server_url: str,
143143
) -> typing.Iterable[Candidate]:
144144
"""Return candidates created from the project name and extras."""
145+
found_candidates: set[str] = set()
146+
ignored_candidates: set[str] = set()
145147
simple_index_url = sdist_server_url.rstrip("/") + "/" + project + "/"
146148
logger.debug("%s: getting available versions from %s", project, simple_index_url)
147149
data = session.get(simple_index_url).content
@@ -151,6 +153,7 @@ def get_project_from_pypi(
151153
py_req = i.attrib.get("data-requires-python")
152154
path = urlparse(candidate_url).path
153155
filename = path.rsplit("/", 1)[-1]
156+
found_candidates.add(filename)
154157
if DEBUG_RESOLVER:
155158
logger.debug("%s: candidate %r -> %r", project, candidate_url, filename)
156159
# Skip items that need a different Python version
@@ -164,12 +167,14 @@ def get_project_from_pypi(
164167
logger.debug(
165168
f"{project}: skipping {filename} because of an invalid python version specifier {py_req}: {err}"
166169
)
170+
ignored_candidates.add(filename)
167171
continue
168172
if PYTHON_VERSION not in spec:
169173
if DEBUG_RESOLVER:
170174
logger.debug(
171175
f"{project}: skipping {filename} because of python version {py_req}"
172176
)
177+
ignored_candidates.add(filename)
173178
continue
174179

175180
# TODO: Handle compatibility tags?
@@ -190,13 +195,15 @@ def get_project_from_pypi(
190195
if not matching_tags:
191196
if DEBUG_RESOLVER:
192197
logger.debug(f"{project}: ignoring {filename} with tags {tags}")
198+
ignored_candidates.add(filename)
193199
continue
194200
except Exception as err:
195201
# Ignore files with invalid versions
196202
if DEBUG_RESOLVER:
197203
logger.debug(
198204
f'{project}: could not determine version for "{filename}": {err}'
199205
)
206+
ignored_candidates.add(filename)
200207
continue
201208
# Look for and ignore cases like `cffi-1.0.2-2.tar.gz` which
202209
# produces the name `cffi-1-0-2`. We can't just compare the
@@ -208,6 +215,7 @@ def get_project_from_pypi(
208215
if len(name) != len(project):
209216
if DEBUG_RESOLVER:
210217
logger.debug(f'{project}: skipping invalid filename "{filename}"')
218+
ignored_candidates.add(filename)
211219
continue
212220

213221
c = Candidate(
@@ -224,6 +232,11 @@ def get_project_from_pypi(
224232
)
225233
yield c
226234

235+
if not found_candidates:
236+
logger.info(f"{project}: found no candidate files at {simple_index_url}")
237+
elif ignored_candidates == found_candidates:
238+
logger.info(f"{project}: ignored all candidate files at {simple_index_url}")
239+
227240

228241
RequirementsMap: typing.TypeAlias = typing.Mapping[str, typing.Iterable[Requirement]]
229242
CandidatesMap: typing.TypeAlias = typing.Mapping[str, typing.Iterable[Candidate]]
@@ -273,37 +286,17 @@ def validate_candidate(
273286
) -> bool:
274287
identifier_reqs = list(requirements[identifier])
275288
bad_versions = {c.version for c in incompatibilities[identifier]}
276-
allow_prerelease = self.constraints.allow_prerelease(identifier)
277-
278289
# Skip versions that are known bad
279290
if candidate.version in bad_versions:
280291
if DEBUG_RESOLVER:
281292
logger.debug(
282293
f"{identifier}: skipping bad version {candidate.version} from {bad_versions}"
283294
)
284295
return False
285-
# Skip versions that do not match the requirement. Allow prereleases only if constraints allow prereleases
286-
if not all(
287-
r.specifier.contains(
288-
candidate.version,
289-
prereleases=(allow_prerelease or bool(r.specifier.prereleases)),
290-
)
291-
for r in identifier_reqs
292-
):
293-
if DEBUG_RESOLVER:
294-
logger.debug(
295-
f"{identifier}: skipping {candidate.version} because it does not match {identifier_reqs}"
296-
)
297-
return False
298-
# Skip versions that do not match the constraint
299-
if not self.constraints.is_satisfied_by(identifier, candidate.version):
300-
if DEBUG_RESOLVER:
301-
c = self.constraints.get_constraint(identifier)
302-
logger.debug(
303-
f"{identifier}: skipping {candidate.version} due to constraint {c}"
304-
)
305-
return False
306-
return True
296+
for r in identifier_reqs:
297+
if self.is_satisfied_by(requirement=r, candidate=candidate):
298+
return True
299+
return False
307300

308301
def get_cache(self) -> dict[str, list[Candidate]]:
309302
raise NotImplementedError()
@@ -350,9 +343,24 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
350343
allow_prerelease = self.constraints.allow_prerelease(requirement.name) or bool(
351344
requirement.specifier.prereleases
352345
)
353-
return requirement.specifier.contains(
346+
if not requirement.specifier.contains(
354347
candidate.version, prereleases=allow_prerelease
355-
) and self.constraints.is_satisfied_by(requirement.name, candidate.version)
348+
):
349+
if DEBUG_RESOLVER:
350+
logger.debug(
351+
f"{requirement.name}: skipping candidate version {candidate.version} because it does not match {requirement.specifier}"
352+
)
353+
return False
354+
355+
if not self.constraints.is_satisfied_by(requirement.name, candidate.version):
356+
if DEBUG_RESOLVER:
357+
c = self.constraints.get_constraint(requirement.name)
358+
logger.debug(
359+
f"{requirement.name}: skipping {candidate.version} due to constraint {c}"
360+
)
361+
return False
362+
363+
return True
356364

357365
def get_dependencies(self, candidate: Candidate) -> list[Requirement]:
358366
# return candidate.dependencies
@@ -437,6 +445,23 @@ def find_matches(
437445
):
438446
candidates.append(candidate)
439447
self.add_to_cache(identifier, candidates)
448+
if not candidates:
449+
# Try to construct a meaningful error message that points out the
450+
# type(s) of files the resolver has been told it can choose as a
451+
# hint in case that should be adjusted for the package that does not
452+
# resolve.
453+
r = next(iter(requirements[identifier]))
454+
if self.include_sdists and self.include_wheels:
455+
raise resolvelib.resolvers.exceptions.ResolverException(
456+
f"found no match for {r}, any file type, in cache or at {self.sdist_server_url}"
457+
)
458+
elif self.include_sdists:
459+
raise resolvelib.resolvers.exceptions.ResolverException(
460+
f"found no match for {r}, limiting search to sdists, in cache or at {self.sdist_server_url}"
461+
)
462+
raise resolvelib.resolvers.exceptions.ResolverException(
463+
f"found no match for {r}, limiting search to wheels, in cache or at {self.sdist_server_url}"
464+
)
440465
return sorted(candidates, key=attrgetter("version", "build_tag"), reverse=True)
441466

442467

tests/test_resolver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def test_provider_constraint_mismatch():
215215
reporter = resolvelib.BaseReporter()
216216
rslvr = resolvelib.Resolver(provider, reporter)
217217

218-
with pytest.raises(resolvelib.resolvers.ResolutionImpossible):
218+
with pytest.raises(resolvelib.resolvers.exceptions.ResolverException):
219219
rslvr.resolve([Requirement("hydra-core")])
220220

221221

0 commit comments

Comments
 (0)