Deduplicate embedded cause suffix in _format_exception#1262
Conversation
…build#1243) resolver.py wraps failures as f"...: {original_msg}" where original_msg is str(err) before chaining `from err` - the cause's raw message ends up embedded at the end of the outer message, anchored on a ": " separator, not the start. _format_exception() currently follows __cause__ unconditionally and always appends " because {cause}", producing a duplicated message for this pattern. Skip the "because" clause when the outer message ends with ": {cause}" (the exact resolver.py construction) - a bare endswith(cause_str) would risk a coincidental match on a short cause (e.g. RuntimeError("failed to match") from ValueError("match")), so the check is anchored on the separator instead. Recursively format the cause first so a deeper cause (the immediate cause's own cause) isn't silently dropped: deduping the immediate cause's raw text but returning early would also discard anything the recursive formatting adds beyond it. If the recursively formatted cause is exactly the raw immediate-cause text, dedup fully; if it's "{cause} because {deeper}", keep the "because {deeper}" suffix. Signed-off-by: Reshmi Aravind <raravind@redhat.com>
📝 WalkthroughWalkthrough
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
What does the error output look like if we stop trying to do the formatting and appending of the "why" chain ourselves and we just let python raise the exception normally and then show the whole traceback? And what would it look like if we use |
|
Good question — I tried the concrete alternatives against the exception shapes we have today. A. Current main after #1241 For a transitive resolution failure, The user-facing line is roughly: B. Simple messages + normal If each exception adds only new information instead of embedding That gives full file/line information at each level, but it is much more verbose for normal CLI output. C. Simple messages + This is the interesting result: In that shape, the original unconditional That suggests the underlying duplication in #1243 is not really caused by original_msg = str(err)
raise ResolverException(f"...: {original_msg}") from errso the same information exists both in the outer message and in A cleaner fix may therefore be to stop embedding Given that, I'm inclined to pause this |
|
Yes, I would like to see what the code changes and output look like if we stop trying to be clever and rely on more standard python implementation. Maybe we want something in the command line tool that takes a chain of exceptions and formats those as a single error message as well as showing the full set of tracebacks. |
Summary
Fixes #1243.
_format_exception()always appends" because {cause}"for anychained exception.
resolver.py:285-288wraps resolution failures asf"Unable to resolve requirement specifier ... using {provider}: {original_msg}"where
original_msgisstr(err), then chainsfrom err- so thecause's own message is already embedded at the end of the outer
message before
_format_exception()ever sees it, producing aduplicated, hard-to-read error:
While reproducing the issue against current
resolver.py, I found thatthe cause text is appended as
f"...: {original_msg}", so the concreterelationship is a suffix rather than a prefix. This uses
endswith(f": {cause_str}")to match that exact construction and avoidcoincidental suffix matches - a bare
endswith(cause_str)wouldincorrectly dedupe something like
RuntimeError("failed to match")chained from
ValueError("match").A naive suffix-dedupe would also silently truncate a deeper cause
chain: if the immediate cause itself has its own cause, deduping
against its raw message alone and returning early loses that deeper
"because ..." information. The fix formats the cause recursively first
and preserves anything beyond the raw immediate-cause text, so a 3+
level chain isn't truncated.
Test plan
pytest tests/test_external_commands.py— covers the resolver-styleduplication case, the unrelated-cause case (unaffected), the
nested-cause preservation case, and the coincidental-suffix guard
case, all with exact-string assertions.
ruff check/ruff format --checkmypy -p fromager— no new errors