From d5762c16efb81c04d83e21e1cbaf917a9b5492a5 2024-08-29 08:27:35 From: M Bussonnier Date: 2024-08-29 08:27:35 Subject: [PATCH] Fix showing SystemExit exception raise inside except handler (#14503) Doing something like this: ```python try: 5 / 0 except Exception as e: raise SystemExit ``` was hitting an error inside UltraTB, creating a long traceback of its internals (which, ironically, UltraTB itself then displays correctly :-). `ListTB.get_exception_only()` calls the `ListTB.structured_traceback()` method *specifically* - even if `self` is a subclass, it won't use the subclass's method. However, the exception chaining in that method uses recursion by calling `self.structured_traceback()`, which will use a subclass's method. Tuples were added as an option there to support exception chaining, but not all of the machinery in connected classes expects a tuple. This just skips the exception chaining logic for the `etb=None` case, when we're showing the exception only. I'm not sure this is necessarily the best fix, but I didn't want to spend too much time following code around a module that's old enough to vote. Closes #12104 --- diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index e167d99..8ed7387 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -298,6 +298,13 @@ except Exception: raise ValueError("Yikes") from None """ + SYS_EXIT_WITH_CONTEXT_CODE = """ +try: + 1/0 +except Exception as e: + raise SystemExit(1) + """ + def test_direct_cause_error(self): with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) @@ -306,6 +313,11 @@ except Exception: with tt.AssertPrints(["KeyError", "NameError", "During handling"]): ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) + def test_sysexit_while_handling_error(self): + with tt.AssertPrints(["SystemExit", "to see the full traceback"]): + with tt.AssertNotPrints(["another exception"], suppress=False): + ip.run_cell(self.SYS_EXIT_WITH_CONTEXT_CODE) + def test_suppress_exception_chaining(self): with tt.AssertNotPrints("ZeroDivisionError"), \ tt.AssertPrints("ValueError", suppress=False): diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index cc139b1..66c9ce9 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -552,28 +552,31 @@ class ListTB(TBTools): lines = ''.join(self._format_exception_only(etype, evalue)) out_list.append(lines) - exception = self.get_parts_of_chained_exception(evalue) + # Find chained exceptions if we have a traceback (not for exception-only mode) + if etb is not None: + exception = self.get_parts_of_chained_exception(evalue) - if exception and (id(exception[1]) not in chained_exc_ids): - chained_exception_message = ( - self.prepare_chained_exception_message(evalue.__cause__)[0] - if evalue is not None - else "" - ) - etype, evalue, etb = exception - # Trace exception to avoid infinite 'cause' loop - chained_exc_ids.add(id(exception[1])) - chained_exceptions_tb_offset = 0 - out_list = ( - self.structured_traceback( - etype, - evalue, - (etb, chained_exc_ids), # type: ignore - chained_exceptions_tb_offset, - context, + if exception and (id(exception[1]) not in chained_exc_ids): + chained_exception_message = ( + self.prepare_chained_exception_message(evalue.__cause__)[0] + if evalue is not None + else "" + ) + etype, evalue, etb = exception + # Trace exception to avoid infinite 'cause' loop + chained_exc_ids.add(id(exception[1])) + chained_exceptions_tb_offset = 0 + out_list = ( + self.structured_traceback( + etype, + evalue, + (etb, chained_exc_ids), # type: ignore + chained_exceptions_tb_offset, + context, + ) + + chained_exception_message + + out_list ) - + chained_exception_message - + out_list) return out_list