https://github.com/python/cpython/commit/84c8cd0f3db37e8409303bd76d6d195d3a1ba6e4
commit: 84c8cd0f3db37e8409303bd76d6d195d3a1ba6e4
branch: 3.13
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2024-08-08T07:26:52Z
summary:

[3.13] gh-87320: In the code module, handle exceptions raised in sys.excepthook 
(GH-122456) (GH-122514)

Before, the exception caused by calling non-default sys.excepthook
in code.InteractiveInterpreter bubbled up to the caller, ending the REPL.
(cherry picked from commit bd3d31f380cd451a4ab6da5fbfde463fed95b5b5)

Co-authored-by: CF Bolz-Tereick <[email protected]>

files:
A Misc/NEWS.d/next/Library/2024-07-30-14-46-16.gh-issue-87320.-Yk1wb.rst
M Lib/code.py
M Lib/test/test_code_module.py
M Lib/test/test_pyrepl/test_pyrepl.py

diff --git a/Lib/code.py b/Lib/code.py
index b93902ccf545b3..7d68517fc5c59a 100644
--- a/Lib/code.py
+++ b/Lib/code.py
@@ -129,7 +129,7 @@ def showsyntaxerror(self, filename=None, **kwargs):
         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 @@ def showtraceback(self, **kwargs):
         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.
 
diff --git a/Lib/test/test_code_module.py b/Lib/test/test_code_module.py
index 259778a5cade98..5dc89108f0ad88 100644
--- a/Lib/test/test_code_module.py
+++ b/Lib/test/test_code_module.py
@@ -77,6 +77,39 @@ def test_sysexcepthook(self):
         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')
diff --git a/Lib/test/test_pyrepl/test_pyrepl.py 
b/Lib/test/test_pyrepl/test_pyrepl.py
index 3a1bacef8a1756..d5eafc5eb58cac 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -1049,6 +1049,30 @@ def test_python_basic_repl(self):
         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
index 00000000000000..4322b719c690c2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-30-14-46-16.gh-issue-87320.-Yk1wb.rst
@@ -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`.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to