Fix hidden traceback entries of chained exceptions getting shown #10921
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This PR is my proposal for fixing #1904 (previous attempt 431ec6d was reverted).
Root cause analysis
The main entry for rendering the traceback for a test exception is
Node._repr_failure_py
. It takes the excinfo and returns aTerminalRepr
. Two parts of it are relevant here:pytest/src/_pytest/nodes.py
Lines 452 to 457 in 4eca606
This calls the node's
_prunetraceback
method with the excinfo, which then proceeds to filter out internal entries, hide__tracebackhide__ = True
frames, etc. It modifies theexcinfo
in-place by updating itsexcinfo.traceback
field.pytest/src/_pytest/nodes.py
Lines 481 to 488 in 4eca606
This calls into the main
FormattedExcinfo
machinery to do the bulk of the work, but passestbfilter=False
because it already did the filtering itself, in aNode
-customizable manner, so doesn't want the generic filtering thatFormattedExcinfo
does.Next, need to look at how
FormattedExcinfo
handles exception chaining.pytest/src/_pytest/_code/code.py
Line 951 in 4eca606
It starts with the excinfo it was passed, which is the main exception. It renders it, then checks if it has any chained exceptions (either
__cause__
or__context__
), and if so, creates anExceptionInfo
for them, renders them, and does the same in a loop.If you've followed closely you see the issue - the main exception got the filtering treatment from the
Node
's_prune_traceback
before it was even passed toFormattedExcinfo
. But the chainedExceptionInfo
's are created insideFormattedExcinfo
, and we've passedtbfilter=False
so they don't even get the basic filtering.Proposed solution
I think it would have been best if the chained
ExceptionInfo
were represented in theExceptionInfo
itself, like how base exceptions work. Then the_prunetraceback
stuff would also prune them before passing toFormattedExcinfo
and all is well. But this is somewhat difficult to achieve, so I went with something easier but less clean.Instead of doing the
_prunetraceback
beforeFormattedExcinfo
, I allowtbfilter
to also be a callback in addition to the existing False/True. Then we passtbfilter=self._prunetraceback
instead of calling it ourselves and passingtbfilter=False
. This then causes the chained exceptions to be filtered exactly like the main exception.Technical notes
As mentioned above, the
_prunetraceback
mutates theexcinfo
in-place. I really didn't like this behavior for thetbfilter
callback. So instead, I changed it to be(ExceptionInfo) -> Traceback
. This breaks the_prunetraceback
interface, so I renamed it to_traceback_filter
.There is also some internal refactoring to make this possible, namely removing the ghastly
WeakReference
cycle which makesTracebackEntry
know about itsExceptionInfo
, and also makingTracebackEntry
immutable. These are separate commits.