Format ExceptionGroup gracefully on Python 3.11+

This commit is contained in:
Ram Rachum 2026-03-08 12:27:09 +02:00
parent 90b2e96ef0
commit 4bff1205c8
3 changed files with 76 additions and 1 deletions

View file

@ -569,7 +569,7 @@ class Tracer:
format(**locals()))
if event == 'exception':
exception = '\n'.join(traceback.format_exception_only(*arg[:2])).strip()
exception = utils.format_exception(*arg[:2])
if self.max_variable_length:
exception = utils.truncate(exception, self.max_variable_length)
self.write('{indent}{_FOREGROUND_RED}Exception:..... '

View file

@ -3,6 +3,7 @@
import abc
import re
import traceback
import sys
from .pycompat import ABC, string_types, collections_abc
@ -87,6 +88,21 @@ def truncate(string, max_length):
return u'{}...{}'.format(string[:left], string[-right:])
def format_exception(exc_type, exc_value):
try:
is_group = isinstance(exc_value, BaseExceptionGroup)
except NameError:
is_group = False
if is_group:
sub_types = ', '.join(type(e).__name__ for e in exc_value.exceptions)
message = exc_value.args[0] if exc_value.args else ''
return u"{}: '{}' ({} sub-exceptions: {})".format(
exc_type.__name__, message,
len(exc_value.exceptions), sub_types,
)
return u'\n'.join(traceback.format_exception_only(exc_type, exc_value)).strip()
def ensure_tuple(x):
if isinstance(x, collections_abc.Iterable) and \
not isinstance(x, string_types):

View file

@ -1908,6 +1908,65 @@ def test_exception():
)
@pytest.mark.skipif(sys.version_info < (3, 11),
reason='ExceptionGroup requires Python 3.11+')
def test_exception_group():
string_io = io.StringIO()
@pysnooper.snoop(string_io, color=False)
def f():
raise ExceptionGroup('task errors', [ValueError('bad'), TypeError('wrong'), RuntimeError('fail')])
with pytest.raises(ExceptionGroup):
f()
output = string_io.getvalue()
assert_output(
output,
(
SourcePathEntry(),
CallEntry(),
LineEntry(),
ExceptionEntry(),
ExceptionValueEntry(
value_regex=r"ExceptionGroup: 'task errors' "
r"\(3 sub-exceptions: ValueError, TypeError, "
r"RuntimeError\)"
),
CallEndedByExceptionEntry(),
ElapsedTimeEntry(),
)
)
@pytest.mark.skipif(sys.version_info < (3, 11),
reason='ExceptionGroup requires Python 3.11+')
def test_nested_exception_group():
string_io = io.StringIO()
@pysnooper.snoop(string_io, color=False)
def f():
raise ExceptionGroup('outer', [ExceptionGroup('inner', [ValueError('deep')]), TypeError('shallow')])
with pytest.raises(ExceptionGroup):
f()
output = string_io.getvalue()
assert_output(
output,
(
SourcePathEntry(),
CallEntry(),
LineEntry(),
ExceptionEntry(),
ExceptionValueEntry(
value_regex=r"ExceptionGroup: 'outer' "
r"\(2 sub-exceptions: ExceptionGroup, TypeError\)"
),
CallEndedByExceptionEntry(),
ElapsedTimeEntry(),
)
)
def test_exception_on_entry():
@pysnooper.snoop(color=False)
def f(x):