Skip to content

Commit 056f2a8

Browse files
authored
Merge pull request #579 from dhellmann/improve-resolver-error-messages
improve error messages from resolver
2 parents 0ed6e75 + fce133c commit 056f2a8

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)