Skip to content

Commit bd3d31f

Browse files
authored
gh-87320: In the code module, handle exceptions raised in sys.excepthook (GH-122456)
Before, the exception caused by calling non-default sys.excepthook in code.InteractiveInterpreter bubbled up to the caller, ending the REPL.
1 parent e60ee11 commit bd3d31f

File tree

4 files changed

+76
-3
lines changed

4 files changed

+76
-3
lines changed

Lib/code.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def showsyntaxerror(self, filename=None, **kwargs):
129129
else:
130130
# If someone has set sys.excepthook, we let that take precedence
131131
# over self.write
132-
sys.excepthook(type, value, tb)
132+
self._call_excepthook(type, value, tb)
133133

134134
def showtraceback(self, **kwargs):
135135
"""Display the exception that just occurred.
@@ -144,16 +144,29 @@ def showtraceback(self, **kwargs):
144144
sys.last_traceback = last_tb
145145
sys.last_exc = ei[1]
146146
try:
147-
lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize)
148147
if sys.excepthook is sys.__excepthook__:
148+
lines = traceback.format_exception(ei[0], ei[1], last_tb.tb_next, colorize=colorize)
149149
self.write(''.join(lines))
150150
else:
151151
# If someone has set sys.excepthook, we let that take precedence
152152
# over self.write
153-
sys.excepthook(ei[0], ei[1], last_tb)
153+
self._call_excepthook(ei[0], ei[1], last_tb)
154154
finally:
155155
last_tb = ei = None
156156

157+
def _call_excepthook(self, typ, value, tb):
158+
try:
159+
sys.excepthook(typ, value, tb)
160+
except SystemExit:
161+
raise
162+
except BaseException as e:
163+
e.__context__ = None
164+
print('Error in sys.excepthook:', file=sys.stderr)
165+
sys.__excepthook__(type(e), e, e.__traceback__.tb_next)
166+
print(file=sys.stderr)
167+
print('Original exception was:', file=sys.stderr)
168+
sys.__excepthook__(typ, value, tb)
169+
157170
def write(self, data):
158171
"""Write a string.
159172

Lib/test/test_code_module.py

+33
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,39 @@ def test_sysexcepthook(self):
7777
self.console.interact()
7878
self.assertTrue(hook.called)
7979

80+
def test_sysexcepthook_crashing_doesnt_close_repl(self):
81+
self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
82+
self.sysmod.excepthook = 1
83+
self.console.interact()
84+
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
85+
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
86+
self.assertIn("Error in sys.excepthook:", error)
87+
self.assertEqual(error.count("'int' object is not callable"), 1)
88+
self.assertIn("Original exception was:", error)
89+
self.assertIn("division by zero", error)
90+
91+
def test_sysexcepthook_raising_BaseException(self):
92+
self.infunc.side_effect = ["1/0", "a = 123", "print(a)", EOFError('Finished')]
93+
s = "not so fast"
94+
def raise_base(*args, **kwargs):
95+
raise BaseException(s)
96+
self.sysmod.excepthook = raise_base
97+
self.console.interact()
98+
self.assertEqual(['write', ('123', ), {}], self.stdout.method_calls[0])
99+
error = "".join(call.args[0] for call in self.stderr.method_calls if call[0] == 'write')
100+
self.assertIn("Error in sys.excepthook:", error)
101+
self.assertEqual(error.count("not so fast"), 1)
102+
self.assertIn("Original exception was:", error)
103+
self.assertIn("division by zero", error)
104+
105+
def test_sysexcepthook_raising_SystemExit_gets_through(self):
106+
self.infunc.side_effect = ["1/0"]
107+
def raise_base(*args, **kwargs):
108+
raise SystemExit
109+
self.sysmod.excepthook = raise_base
110+
with self.assertRaises(SystemExit):
111+
self.console.interact()
112+
80113
def test_banner(self):
81114
# with banner
82115
self.infunc.side_effect = EOFError('Finished')

Lib/test/test_pyrepl/test_pyrepl.py

+24
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,30 @@ def test_python_basic_repl(self):
10491049
self.assertNotIn("Exception", output)
10501050
self.assertNotIn("Traceback", output)
10511051

1052+
@force_not_colorized
1053+
def test_bad_sys_excepthook_doesnt_crash_pyrepl(self):
1054+
env = os.environ.copy()
1055+
commands = ("import sys\n"
1056+
"sys.excepthook = 1\n"
1057+
"1/0\n"
1058+
"exit()\n")
1059+
1060+
def check(output, exitcode):
1061+
self.assertIn("Error in sys.excepthook:", output)
1062+
self.assertEqual(output.count("'int' object is not callable"), 1)
1063+
self.assertIn("Original exception was:", output)
1064+
self.assertIn("division by zero", output)
1065+
self.assertEqual(exitcode, 0)
1066+
env.pop("PYTHON_BASIC_REPL", None)
1067+
output, exit_code = self.run_repl(commands, env=env)
1068+
if "can\'t use pyrepl" in output:
1069+
self.skipTest("pyrepl not available")
1070+
check(output, exit_code)
1071+
1072+
env["PYTHON_BASIC_REPL"] = "1"
1073+
output, exit_code = self.run_repl(commands, env=env)
1074+
check(output, exit_code)
1075+
10521076
def test_not_wiping_history_file(self):
10531077
# skip, if readline module is not available
10541078
import_module('readline')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In :class:`code.InteractiveInterpreter`, handle exceptions caused by calling a
2+
non-default :func:`sys.excepthook`. Before, the exception bubbled up to the
3+
caller, ending the :term:`REPL`.

0 commit comments

Comments
 (0)