diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 1f38a43be51e7a..8e9f1c3204a8bb 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -1,6 +1,7 @@ import contextlib import os import pickle +import signal import sys from textwrap import dedent import threading @@ -11,7 +12,7 @@ from test.support import os_helper from test.support import script_helper from test.support import import_helper -from test.support.script_helper import assert_python_ok +from test.support.script_helper import assert_python_ok, spawn_python # Raise SkipTest if subinterpreters not supported. _interpreters = import_helper.import_module('_interpreters') from concurrent import interpreters @@ -434,6 +435,31 @@ def test_cleanup_in_repl(self): self.assertIn(b"remaining subinterpreters", stdout) self.assertNotIn(b"Traceback", stdout) + @support.requires_subprocess() + @unittest.skipIf(os.name == 'nt', "signals don't work well on windows") + def test_keyboard_interrupt_in_thread_running_interp(self): + import subprocess + source = f"""if True: + from concurrent import interpreters + from threading import Thread + + def test(): + import time + print('a', flush=True, end='') + time.sleep(10) + + interp = interpreters.create() + interp.call_in_thread(test) + """ + + with spawn_python("-c", source, stderr=subprocess.PIPE) as proc: + self.assertEqual(proc.stdout.read(1), b'a') + proc.send_signal(signal.SIGINT) + proc.stderr.flush() + error = proc.stderr.read() + self.assertIn(b"KeyboardInterrupt", error) + retcode = proc.wait() + self.assertEqual(retcode, 0) class TestInterpreterIsRunning(TestBase): diff --git a/Misc/NEWS.d/next/Library/2025-09-19-07-41-52.gh-issue-126016.Uz9W6h.rst b/Misc/NEWS.d/next/Library/2025-09-19-07-41-52.gh-issue-126016.Uz9W6h.rst new file mode 100644 index 00000000000000..feb09294bec982 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-19-07-41-52.gh-issue-126016.Uz9W6h.rst @@ -0,0 +1,2 @@ +Fix an assertion failure when sending :exc:`KeyboardInterrupt` to a Python +process running a subinterpreter in a separate thread. diff --git a/Python/pystate.c b/Python/pystate.c index 29c713dccc9fe8..dbed609f29aa07 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -1625,7 +1625,11 @@ PyThreadState_Clear(PyThreadState *tstate) { assert(tstate->_status.initialized && !tstate->_status.cleared); assert(current_fast_get()->interp == tstate->interp); - assert(!_PyThreadState_IsRunningMain(tstate)); + // GH-126016: In the _interpreters module, KeyboardInterrupt exceptions + // during PyEval_EvalCode() are sent to finalization, which doesn't let us + // mark threads as "not running main". So, for now this assertion is + // disabled. + // XXX assert(!_PyThreadState_IsRunningMain(tstate)); // XXX assert(!tstate->_status.bound || tstate->_status.unbound); tstate->_status.finalizing = 1; // just in case