Skip to content

bpo-31299: Make it possible to filter out frames from tracebacks #26772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Doc/library/traceback.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ The module also defines the following classes:
:class:`TracebackException` objects are created from actual exceptions to
capture data for later printing in a lightweight fashion.

.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False)
.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False, stack_summary_cls=None)

Capture an exception for later rendering. *limit*, *lookup_lines* and
*capture_locals* are as for the :class:`StackSummary` class.
Expand All @@ -222,6 +222,10 @@ capture data for later printing in a lightweight fashion.
``__context__`` field is calculated only if ``__cause__`` is ``None`` and
``__suppress_context__`` is false.

If *stack_summary_cls* is not ``None``, it is a class to be used instead of
the default :class:`~traceback.StackSummary` to format the stack (typically
a subclass that overrides :meth:`~traceback.StackSummary.format_frame`).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a code example, in particular, class SkipG from the test, which skips frames for a function named 'g'.

Note that when locals are captured, they are also shown in the traceback.

.. attribute:: __cause__
Expand Down Expand Up @@ -309,6 +313,8 @@ capture data for later printing in a lightweight fashion.
.. versionchanged:: 3.10
Added the *compact* parameter.

.. versionchanged:: 3.11
Added the *stack_summary_cls* paramter.

:class:`StackSummary` Objects
-----------------------------
Expand Down Expand Up @@ -357,7 +363,8 @@ capture data for later printing in a lightweight fashion.

Returns a string for printing one of the frames involved in the stack.
This method gets called for each frame object to be printed in the
:class:`StackSummary`.
:class:`StackSummary`. If it returns ``none``, the frame is omitted
from the output.

.. versionadded:: 3.11

Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,40 @@ def some_inner():
s.format(),
[f'{__file__}:{some_inner.__code__.co_firstlineno + 1}'])

def test_dropping_frames(self):
def f():
1/0
def g():
try:
f()
except:
return sys.exc_info()
exc_info = g()

class Skip_G(traceback.StackSummary):
def format_frame(self, frame):
if frame.name == 'g':
return None
return super().format_frame(frame)

def get_output(stack_summary_cls=None):
output = StringIO()
traceback.TracebackException(
*exc_info, stack_summary_cls=stack_summary_cls,
).print(file=output)
return output.getvalue().split('\n')

default = get_output()
skip_g = get_output(Skip_G)

for l in skip_g:
default.remove(l)
# Only the lines for g's frame should remain:
self.assertEqual(len(default), 3)
self.assertRegex(default[0], ', line [0-9]*, in g')
self.assertEqual(default[1], ' f()')
self.assertEqual(default[2], ' ^^^')


class TestTracebackException(unittest.TestCase):

Expand Down
27 changes: 18 additions & 9 deletions Lib/traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,8 @@ def format_frame(self, frame):
"""Format the lines for a single frame.

Returns a string representing one frame involved in the stack. This
gets called for every frame to be printed in the stack summary.
gets called for every frame to be printed in the stack summary. If
it returns ``None``, the frame is omitted from the output.
"""
row = []
row.append(' File "{}", line {}, in {}\n'.format(
Expand Down Expand Up @@ -507,25 +508,32 @@ def format(self):
last_file = None
last_line = None
last_name = None
last_line_displayed = False
count = 0
for frame in self:
if (last_file is None or last_file != frame.filename or
last_line is None or last_line != frame.lineno or
last_name is None or last_name != frame.name):
if count > _RECURSIVE_CUTOFF:
count -= _RECURSIVE_CUTOFF
result.append(
f' [Previous line repeated {count} more '
f'time{"s" if count > 1 else ""}]\n'
)
if last_line_displayed:
result.append(
f' [Previous line repeated {count} more '
f'time{"s" if count > 1 else ""}]\n'
)
last_file = frame.filename
last_line = frame.lineno
last_name = frame.name
count = 0
count += 1
if count > _RECURSIVE_CUTOFF:
continue
result.append(self.format_frame(frame))
formatted_frame = self.format_frame(frame)
if formatted_frame is not None:
last_line_displayed = True
result.append(formatted_frame)
else:
last_line_displayed = False

if count > _RECURSIVE_CUTOFF:
count -= _RECURSIVE_CUTOFF
Expand Down Expand Up @@ -618,7 +626,7 @@ class TracebackException:

def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
lookup_lines=True, capture_locals=False, compact=False,
_seen=None):
stack_summary_cls=None, _seen=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a good approach to customizing the behavior while reusing most of the exception printing code. There's precedent for this style of expansion with JSONEncoder and cls in the json module. Any thoughts @terryjreedy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See general review comment.

# NB: we need to accept exc_traceback, exc_value, exc_traceback to
# permit backwards compat with the existing API, otherwise we
# need stub thunk objects just to glue it together.
Expand All @@ -628,8 +636,9 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
_seen = set()
_seen.add(id(exc_value))

# TODO: locals.
self.stack = StackSummary._extract_from_extended_frame_gen(
if stack_summary_cls is None:
stack_summary_cls = StackSummary
self.stack = stack_summary_cls._extract_from_extended_frame_gen(
_walk_tb_with_full_positions(exc_traceback),
limit=limit, lookup_lines=lookup_lines,
capture_locals=capture_locals)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added the ``stack_summary_cls`` parameter to :class:`TracebackException`, to allow fine-grained control over the content of a formatted traceback. Added option to completely drop frames from the output by returning ``None`` from a :meth:`~StackSummary.format_frame` override.