]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-87320: In the code module, handle exceptions raised in sys.excepthook (GH-122456) 122501/head
authorCF Bolz-Tereick <cfbolz@gmx.de>
Wed, 31 Jul 2024 10:33:29 +0000 (12:33 +0200)
committerGitHub <noreply@github.com>
Wed, 31 Jul 2024 10:33:29 +0000 (10:33 +0000)
Before, the exception caused by calling non-default sys.excepthook
in code.InteractiveInterpreter bubbled up to the caller, ending the REPL.

Lib/code.py
Lib/test/test_code_module.py
Lib/test/test_pyrepl/test_pyrepl.py
Misc/NEWS.d/next/Library/2024-07-30-14-46-16.gh-issue-87320.-Yk1wb.rst [new file with mode: 0644]

index a55fced0704b1dc929c2e8318d8ad060c0070627..cd3889067d068db37282bb73078723cdf1832f99 100644 (file)
@@ -129,7 +129,7 @@ class InteractiveInterpreter:
         else:
             # If someone has set sys.excepthook, we let that take precedence
             # over self.write
-            sys.excepthook(type, value, tb)
+            self._call_excepthook(type, value, tb)
 
     def showtraceback(self, **kwargs):
         """Display the exception that just occurred.
@@ -144,16 +144,29 @@ class InteractiveInterpreter:
         sys.last_traceback = last_tb
         sys.last_exc = ei[1]
         try:
-            lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize)
             if sys.excepthook is sys.__excepthook__:
+                lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize)
                 self.write(''.join(lines))
             else:
                 # If someone has set sys.excepthook, we let that take precedence
                 # over self.write
-                sys.excepthook(ei[0], ei[1], last_tb)
+                self._call_excepthook(ei[0], ei[1], last_tb)
         finally:
             last_tb = ei = None
 
+    def _call_excepthook(self, typ, value, tb):
+        try:
+            sys.excepthook(typ, value, tb)
+        except SystemExit:
+            raise
+        except BaseException as e:
+            e.__context__ = None
+            print('Error in sys.excepthook:', file=sys.stderr)
+            sys.__excepthook__(type(e), e, e.__traceback__.tb_next)
+            print(file=sys.stderr)
+            print('Original exception was:', file=sys.stderr)
+            sys.__excepthook__(typ, value, tb)
+
     def write(self, data):
         """Write a string.
 
index 259778a5cade98feb9d84befdc9d2a1f11c91c38..5dc89108f0ad889f14a4b36eb0608077f222c720 100644 (file)
@@ -77,6 +77,39 @@ class TestInteractiveConsole(unittest.TestCase, MockSys):
         self.console.interact()
         self.assertTrue(hook.called)
 
+    def test_sysexcepthook_crashing_doesnt_close_repl(self):
+        self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
+        self.sysmod.excepthook = 1
+        self.console.interact()
+        self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
+        error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
+        self.assertIn("Error in sys.excepthook:", error)
+        self.assertEqual(error.count("'int' object is not callable"), 1)
+        self.assertIn("Original exception was:", error)
+        self.assertIn("division by zero", error)
+
+    def test_sysexcepthook_raising_BaseException(self):
+        self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
+        s = "not so fast"
+        def raise_base(*args, **kwargs):
+            raise BaseException(s)
+        self.sysmod.excepthook = raise_base
+        self.console.interact()
+        self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
+        error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
+        self.assertIn("Error in sys.excepthook:", error)
+        self.assertEqual(error.count("not so fast"), 1)
+        self.assertIn("Original exception was:", error)
+        self.assertIn("division by zero", error)
+
+    def test_sysexcepthook_raising_SystemExit_gets_through(self):
+        self.infunc.side_effect = ["1/0"]
+        def raise_base(*args, **kwargs):
+            raise SystemExit
+        self.sysmod.excepthook = raise_base
+        with self.assertRaises(SystemExit):
+            self.console.interact()
+
     def test_banner(self):
         # with banner
         self.infunc.side_effect = EOFError('Finished')
index 3a1bacef8a1756f8de70bd041e76f60cdda100f5..d5eafc5eb58cac268ee234521875ac7832f9cc6d 100644 (file)
@@ -1049,6 +1049,30 @@ class TestMain(TestCase):
         self.assertNotIn("Exception", output)
         self.assertNotIn("Traceback", output)
 
+    @force_not_colorized
+    def test_bad_sys_excepthook_doesnt_crash_pyrepl(self):
+        env = os.environ.copy()
+        commands = ("import sys\n"
+                    "sys.excepthook = 1\n"
+                    "1/0\n"
+                    "exit()\n")
+
+        def check(output, exitcode):
+            self.assertIn("Error in sys.excepthook:", output)
+            self.assertEqual(output.count("'int' object is not callable"), 1)
+            self.assertIn("Original exception was:", output)
+            self.assertIn("division by zero", output)
+            self.assertEqual(exitcode, 0)
+        env.pop("PYTHON_BASIC_REPL", None)
+        output, exit_code = self.run_repl(commands, env=env)
+        if "can\'t use pyrepl" in output:
+            self.skipTest("pyrepl not available")
+        check(output, exit_code)
+
+        env["PYTHON_BASIC_REPL"] = "1"
+        output, exit_code = self.run_repl(commands, env=env)
+        check(output, exit_code)
+
     def test_not_wiping_history_file(self):
         # skip, if readline module is not available
         import_module('readline')
diff --git a/Misc/NEWS.d/next/Library/2024-07-30-14-46-16.gh-issue-87320.-Yk1wb.rst b/Misc/NEWS.d/next/Library/2024-07-30-14-46-16.gh-issue-87320.-Yk1wb.rst
new file mode 100644 (file)
index 0000000..4322b71
--- /dev/null
@@ -0,0 +1,3 @@
+In :class:`code.InteractiveInterpreter`, handle exceptions caused by calling a
+non-default :func:`sys.excepthook`. Before, the exception bubbled up to the
+caller, ending the :term:`REPL`.