]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-132308: prevent `TracebackException` swallowing attributes of a falsey...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Wed, 30 Apr 2025 07:19:53 +0000 (09:19 +0200)
committerGitHub <noreply@github.com>
Wed, 30 Apr 2025 07:19:53 +0000 (09:19 +0200)
gh-132308: prevent `TracebackException` swallowing attributes of a falsey `Exception` or `ExceptionGroup` (GH-132363)
(cherry picked from commit 69cda31261dd98b0462dc5ca63bdbcd0954dfa77)

Co-authored-by: Duprat <yduprat@gmail.com>
Lib/test/test_traceback.py
Lib/traceback.py
Misc/NEWS.d/next/Library/2025-04-10-13-06-42.gh-issue-132308.1js5SI.rst [new file with mode: 0644]

index 2dfee81681d42d9d7b9d0fd26c0799cd8f6aacde..e906fcc17c6c5d89300d88506dc21d12402a6b52 100644 (file)
@@ -3408,6 +3408,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:
@@ -3753,6 +3766,24 @@ class TestTracebackException(unittest.TestCase):
              '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):
@@ -3954,6 +3985,26 @@ class TestTracebackException_ExceptionGroups(unittest.TestCase):
         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
 
index 15f59bba54d0db01e8907ce75420cf72b98c3b73..12235a8d93ea5c175acd1e77dc575e47b8c87b94 100644 (file)
@@ -1116,7 +1116,7 @@ class TracebackException:
             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__),
@@ -1137,7 +1137,7 @@ class TracebackException:
                                     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__),
@@ -1152,7 +1152,7 @@ class TracebackException:
                 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 (file)
index 0000000..8e8b99c
--- /dev/null
@@ -0,0 +1,3 @@
+A :class:`traceback.TracebackException` now correctly renders the  ``__context__``
+and ``__cause__`` attributes from :ref:`falsey <truth>` :class:`Exception`,
+and the ``exceptions`` attribute from falsey :class:`ExceptionGroup`.