Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions Lib/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_code_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
24 changes: 24 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
Loading