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.
This commit is contained in:
Irit Katriel 2021-01-15 02:45:02 +00:00 committed by GitHub
parent e5fe509054
commit 4c94d74152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 10 deletions

View File

@ -212,11 +212,16 @@ 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)
.. class:: TracebackException(exc_type, exc_value, exc_traceback, *, limit=None, lookup_lines=True, capture_locals=False, compact=False)
Capture an exception for later rendering. *limit*, *lookup_lines* and
*capture_locals* are as for the :class:`StackSummary` class.
If *compact* is true, only data that is required by :class:`TracebackException`'s
``format`` method is saved in the class attributes. In particular, the
``__context__`` field is calculated only if ``__cause__`` is ``None`` and
``__suppress_context__`` is false.
Note that when locals are captured, they are also shown in the traceback.
.. attribute:: __cause__
@ -294,6 +299,9 @@ capture data for later printing in a lightweight fashion.
The message indicating which exception occurred is always the last
string in the output.
.. versionchanged:: 3.10
Added the *compact* parameter.
:class:`StackSummary` Objects
-----------------------------

View File

@ -1173,6 +1173,46 @@ class TestTracebackException(unittest.TestCase):
self.assertIn(
"RecursionError: maximum recursion depth exceeded", res[-1])
def test_compact_with_cause(self):
try:
try:
1/0
finally:
cause = Exception("cause")
raise Exception("uh oh") from cause
except Exception:
exc_info = sys.exc_info()
exc = traceback.TracebackException(*exc_info, compact=True)
expected_stack = traceback.StackSummary.extract(
traceback.walk_tb(exc_info[2]))
exc_cause = traceback.TracebackException(Exception, cause, None)
self.assertEqual(exc_cause, exc.__cause__)
self.assertEqual(None, exc.__context__)
self.assertEqual(True, exc.__suppress_context__)
self.assertEqual(expected_stack, exc.stack)
self.assertEqual(exc_info[0], exc.exc_type)
self.assertEqual(str(exc_info[1]), str(exc))
def test_compact_no_cause(self):
try:
try:
1/0
finally:
exc_info_context = sys.exc_info()
exc_context = traceback.TracebackException(*exc_info_context)
raise Exception("uh oh")
except Exception:
exc_info = sys.exc_info()
exc = traceback.TracebackException(*exc_info, compact=True)
expected_stack = traceback.StackSummary.extract(
traceback.walk_tb(exc_info[2]))
self.assertEqual(None, exc.__cause__)
self.assertEqual(exc_context, exc.__context__)
self.assertEqual(False, exc.__suppress_context__)
self.assertEqual(expected_stack, exc.stack)
self.assertEqual(exc_info[0], exc.exc_type)
self.assertEqual(str(exc_info[1]), str(exc))
def test_no_refs_to_exception_and_traceback_objects(self):
try:
1/0

View File

@ -110,8 +110,8 @@ def print_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
value, tb = _parse_value_tb(exc, value, tb)
if file is None:
file = sys.stderr
for line in TracebackException(
type(value), value, tb, limit=limit).format(chain=chain):
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
for line in te.format(chain=chain):
print(line, file=file, end="")
@ -126,8 +126,8 @@ def format_exception(exc, /, value=_sentinel, tb=_sentinel, limit=None, \
printed as does print_exception().
"""
value, tb = _parse_value_tb(exc, value, tb)
return list(TracebackException(
type(value), value, tb, limit=limit).format(chain=chain))
te = TracebackException(type(value), value, tb, limit=limit, compact=True)
return list(te.format(chain=chain))
def format_exception_only(exc, /, value=_sentinel):
@ -146,8 +146,8 @@ def format_exception_only(exc, /, value=_sentinel):
"""
if value is _sentinel:
value = exc
return list(TracebackException(
type(value), value, None).format_exception_only())
te = TracebackException(type(value), value, None, compact=True)
return list(te.format_exception_only())
# -- not official API but folk probably use these two functions.
@ -476,7 +476,8 @@ class TracebackException:
"""
def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None,
lookup_lines=True, capture_locals=False, _seen=None):
lookup_lines=True, capture_locals=False, compact=False,
_seen=None):
# 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.
@ -485,6 +486,7 @@ class TracebackException:
if _seen is None:
_seen = set()
_seen.add(id(exc_value))
# TODO: locals.
self.stack = StackSummary.extract(
walk_tb(exc_traceback), limit=limit, lookup_lines=lookup_lines,
@ -504,7 +506,7 @@ class TracebackException:
if lookup_lines:
self._load_lines()
self.__suppress_context__ = \
exc_value.__suppress_context__ if exc_value else False
exc_value.__suppress_context__ if exc_value is not None else False
# Convert __cause__ and __context__ to `TracebackExceptions`s, use a
# queue to avoid recursion (only the top-level call gets _seen == None)
@ -524,8 +526,13 @@ class TracebackException:
_seen=_seen)
else:
cause = None
if compact:
need_context = cause is None and not e.__suppress_context__
else:
need_context = True
if (e and e.__context__ is not None
and id(e.__context__) not in _seen):
and need_context and id(e.__context__) not in _seen):
context = TracebackException(
type(e.__context__),
e.__context__,

View File

@ -0,0 +1,4 @@
Added the ``compact`` parameter to the constructor of
:class:`traceback.TracebackException` to reduce time and memory
for use cases that only need to call :func:`TracebackException.format`
and :func:`TracebackException.format_exception_only`.