diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 5c390fd056b3e3..a806dbf1582268 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3413,6 +3413,19 @@ class Unrepresentable: def __repr__(self) -> str: raise Exception("Unrepresentable") + +# Used in test_dont_swallow_cause_or_context_of_falsey_exception and +# test_dont_swallow_subexceptions_of_falsey_exceptiongroup. +class FalseyException(Exception): + def __bool__(self): + return False + + +class FalseyExceptionGroup(ExceptionGroup): + def __bool__(self): + return False + + class TestTracebackException(unittest.TestCase): def do_test_smoke(self, exc, expected_type_str): try: @@ -3759,6 +3772,24 @@ def f(): 'ZeroDivisionError: division by zero', '']) + def test_dont_swallow_cause_or_context_of_falsey_exception(self): + # see gh-132308: Ensure that __cause__ or __context__ attributes of exceptions + # that evaluate as falsey are included in the output. For falsey term, + # see https://docs.python.org/3/library/stdtypes.html#truth-value-testing. + + try: + raise FalseyException from KeyError + except FalseyException as e: + self.assertIn(cause_message, traceback.format_exception(e)) + + try: + try: + 1/0 + except ZeroDivisionError: + raise FalseyException + except FalseyException as e: + self.assertIn(context_message, traceback.format_exception(e)) + class TestTracebackException_ExceptionGroups(unittest.TestCase): def setUp(self): @@ -3960,6 +3991,26 @@ def test_comparison(self): self.assertNotEqual(exc, object()) self.assertEqual(exc, ALWAYS_EQ) + def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): + # see gh-132308: Ensure that subexceptions of exception groups + # that evaluate as falsey are displayed in the output. For falsey term, + # see https://docs.python.org/3/library/stdtypes.html#truth-value-testing. + + try: + raise FalseyExceptionGroup("Gih", (KeyError(), NameError())) + except Exception as ee: + str_exc = ''.join(traceback.format_exception(ee)) + self.assertIn('+---------------- 1 ----------------', str_exc) + self.assertIn('+---------------- 2 ----------------', str_exc) + + # Test with a falsey exception, in last position, as sub-exceptions. + msg = 'bool' + try: + raise FalseyExceptionGroup("Gah", (KeyError(), FalseyException(msg))) + except Exception as ee: + str_exc = traceback.format_exception(ee) + self.assertIn(f'{FalseyException.__name__}: {msg}', str_exc[-2]) + global_for_suggestions = None diff --git a/Lib/traceback.py b/Lib/traceback.py index 647c23ed782c41..78c35136ea9e8c 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -1120,7 +1120,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, queue = [(self, exc_value)] while queue: te, e = queue.pop() - if (e and e.__cause__ is not None + if (e is not None and e.__cause__ is not None and id(e.__cause__) not in _seen): cause = TracebackException( type(e.__cause__), @@ -1141,7 +1141,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, not e.__suppress_context__) else: need_context = True - if (e and e.__context__ is not None + if (e is not None and e.__context__ is not None and need_context and id(e.__context__) not in _seen): context = TracebackException( type(e.__context__), @@ -1156,7 +1156,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, *, limit=None, else: context = None - if e and isinstance(e, BaseExceptionGroup): + if e is not None and isinstance(e, BaseExceptionGroup): exceptions = [] for exc in e.exceptions: texc = TracebackException( diff --git a/Misc/NEWS.d/next/Library/2025-04-10-13-06-42.gh-issue-132308.1js5SI.rst b/Misc/NEWS.d/next/Library/2025-04-10-13-06-42.gh-issue-132308.1js5SI.rst new file mode 100644 index 00000000000000..8e8b99c2be31ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-10-13-06-42.gh-issue-132308.1js5SI.rst @@ -0,0 +1,3 @@ +A :class:`traceback.TracebackException` now correctly renders the ``__context__`` +and ``__cause__`` attributes from :ref:`falsey ` :class:`Exception`, +and the ``exceptions`` attribute from falsey :class:`ExceptionGroup`.