Skip to content

Commit 4c94d74

Browse files
authored
bpo-42877: add the 'compact' param to TracebackException's __init__ (#24179)
Use it to reduce the time and memory taken up by several of traceback's module-level functions.
1 parent e5fe509 commit 4c94d74

File tree

4 files changed

+69
-10
lines changed

4 files changed

+69
-10
lines changed

Doc/library/traceback.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,16 @@ The module also defines the following classes:
212212
:class:`TracebackException` objects are created from actual exceptions to
213213
capture data for later printing in a lightweight fashion.
214214

215-
.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False)
215+
.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False)
216216

217217
Capture an exception for later rendering. *limit*, *lookup_lines* and
218218
*capture_locals* are as for the :class:`StackSummary` class.
219219

220+
If *compact* is true, only data that is required by :class:`TracebackException`'s
221+
``format`` method is saved in the class attributes. In particular, the
222+
``__context__`` field is calculated only if ``__cause__`` is ``None`` and
223+
``__suppress_context__`` is false.
224+
220225
Note that when locals are captured, they are also shown in the traceback.
221226

222227
.. attribute:: __cause__
@@ -294,6 +299,9 @@ capture data for later printing in a lightweight fashion.
294299
The message indicating which exception occurred is always the last
295300
string in the output.
296301

302+
.. versionchanged:: 3.10
303+
Added the *compact* parameter.
304+
297305

298306
:class:`StackSummary` Objects
299307
-----------------------------

Lib/test/test_traceback.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,46 @@ def f():
11731173
self.assertIn(
11741174
"RecursionError: maximum recursion depth exceeded", res[-1])
11751175

1176+
def test_compact_with_cause(self):
1177+
try:
1178+
try:
1179+
1/0
1180+
finally:
1181+
cause = Exception("cause")
1182+
raise Exception("uh oh") from cause
1183+
except Exception:
1184+
exc_info = sys.exc_info()
1185+
exc = traceback.TracebackException(*exc_info, compact=True)
1186+
expected_stack = traceback.StackSummary.extract(
1187+
traceback.walk_tb(exc_info[2]))
1188+
exc_cause = traceback.TracebackException(Exception, cause, None)
1189+
self.assertEqual(exc_cause, exc.__cause__)
1190+
self.assertEqual(None, exc.__context__)
1191+
self.assertEqual(True, exc.__suppress_context__)
1192+
self.assertEqual(expected_stack, exc.stack)
1193+
self.assertEqual(exc_info[0], exc.exc_type)
1194+
self.assertEqual(str(exc_info[1]), str(exc))
1195+
1196+
def test_compact_no_cause(self):
1197+
try:
1198+
try:
1199+
1/0
1200+
finally:
1201+
exc_info_context = sys.exc_info()
1202+
exc_context = traceback.TracebackException(*exc_info_context)
1203+
raise Exception("uh oh")
1204+
except Exception:
1205+
exc_info = sys.exc_info()
1206+
exc = traceback.TracebackException(*exc_info, compact=True)
1207+
expected_stack = traceback.StackSummary.extract(
1208+
traceback.walk_tb(exc_info[2]))
1209+
self.assertEqual(None, exc.__cause__)
1210+
self.assertEqual(exc_context, exc.__context__)
1211+
self.assertEqual(False, exc.__suppress_context__)
1212+
self.assertEqual(expected_stack, exc.stack)
1213+
self.assertEqual(exc_info[0], exc.exc_type)
1214+
self.assertEqual(str(exc_info[1]), str(exc))
1215+
11761216
def test_no_refs_to_exception_and_traceback_objects(self):
11771217
try:
11781218
1/0

Lib/traceback.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
110110
value, tb = _parse_value_tb(exc, value, tb)
111111
if file is None:
112112
file = sys.stderr
113-
for line in TracebackException(
114-
type(value), value, tb, limit=limit).format(chain=chain):
113+
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
114+
for line in te.format(chain=chain):
115115
print(line, file=file, end="")
116116

117117

@@ -126,8 +126,8 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
126126
printed as does print_exception().
127127
"""
128128
value, tb = _parse_value_tb(exc, value, tb)
129-
return list(TracebackException(
130-
type(value), value, tb, limit=limit).format(chain=chain))
129+
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
130+
return list(te.format(chain=chain))
131131

132132

133133
def format_exception_only(exc, /, value=_sentinel):
@@ -146,8 +146,8 @@ def format_exception_only(exc, /, value=_sentinel):
146146
"""
147147
if value is _sentinel:
148148
value = exc
149-
return list(TracebackException(
150-
type(value), value, None).format_exception_only())
149+
te = TracebackException(type(value), value, None, compact=True)
150+
return list(te.format_exception_only())
151151

152152

153153
# -- not official API but folk probably use these two functions.
@@ -476,7 +476,8 @@ class TracebackException:
476476
"""
477477

478478
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
479-
lookup_lines=True, capture_locals=False, _seen=None):
479+
lookup_lines=True, capture_locals=False, compact=False,
480+
_seen=None):
480481
# NB: we need to accept exc_traceback, exc_value, exc_traceback to
481482
# permit backwards compat with the existing API, otherwise we
482483
# need stub thunk objects just to glue it together.
@@ -485,6 +486,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
485486
if _seen is None:
486487
_seen = set()
487488
_seen.add(id(exc_value))
489+
488490
# TODO: locals.
489491
self.stack = StackSummary.extract(
490492
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
@@ -504,7 +506,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
504506
if lookup_lines:
505507
self._load_lines()
506508
self.__suppress_context__ = \
507-
exc_value.__suppress_context__ if exc_value else False
509+
exc_value.__suppress_context__ if exc_value is not None else False
508510

509511
# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
510512
# queue to avoid recursion (only the top-level call gets _seen == None)
@@ -524,8 +526,13 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
524526
_seen=_seen)
525527
else:
526528
cause = None
529+
530+
if compact:
531+
need_context = cause is None and not e.__suppress_context__
532+
else:
533+
need_context = True
527534
if (e and e.__context__ is not None
528-
and id(e.__context__) not in _seen):
535+
and need_context and id(e.__context__) not in _seen):
529536
context = TracebackException(
530537
type(e.__context__),
531538
e.__context__,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added the ``compact`` parameter to the constructor of
2+
:class:`traceback.TracebackException` to reduce time and memory
3+
for use cases that only need to call :func:`TracebackException.format`
4+
and :func:`TracebackException.format_exception_only`.

0 commit comments

Comments
 (0)