diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index c79f2e71ba9da5..257f9ee1f5c410 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -1895,6 +1895,15 @@ Changes in the Python API * Mixing tabs and spaces as indentation in the same file is not supported anymore and will raise a :exc:`TabError`. +* The :mod:`threading` module now expects the :mod:`!_thread` module to have + an ``_is_main_interpreter`` attribute. It is a function with no + arguments that returns ``True`` if the current interpreter is the + main interpreter. + + Any library or application that provides a custom ``_thread`` module + should provide ``_is_main_interpreter()``. + (See :gh:`112826`.) + Build Changes ============= diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index ce477d2df7d7d4..756d5e329fc6d3 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -1827,6 +1827,34 @@ def test__all__(self): support.check__all__(self, threading, ('threading', '_thread'), extra=extra, not_exported=not_exported) + @requires_subprocess() + def test_gh112826_missing__thread__is_main_interpreter(self): + with os_helper.temp_dir() as tempdir: + modname = '_thread_fake' + import os.path + filename = os.path.join(tempdir, modname + '.py') + with open(filename, 'w') as outfile: + outfile.write("""if True: + import _thread + globals().update(vars(_thread)) + del _is_main_interpreter + """) + + expected_output = b'success!' + _, out, err = assert_python_ok("-c", f"""if True: + import sys + sys.path.insert(0, {tempdir!r}) + import {modname} + sys.modules['_thread'] = {modname} + del sys.modules[{modname!r}] + + import threading + print({expected_output.decode('utf-8')!r}, end='') + """) + + self.assertEqual(out, expected_output) + self.assertEqual(err, b'') + class InterruptMainTests(unittest.TestCase): def check_interrupt_main_with_signal_handler(self, signum): diff --git a/Lib/threading.py b/Lib/threading.py index 624e7ed8f07c8f..8dcaf8ca6a03c6 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -37,7 +37,20 @@ _allocate_lock = _thread.allocate_lock _set_sentinel = _thread._set_sentinel get_ident = _thread.get_ident -_is_main_interpreter = _thread._is_main_interpreter +try: + _is_main_interpreter = _thread._is_main_interpreter +except AttributeError: + # See https://github.com/python/cpython/issues/112826. + # We can pretend a subinterpreter is the main interpreter for the + # sake of _shutdown(), since that only means we do not wait for the + # subinterpreter's threads to finish. Instead, they will be stopped + # later by the mechanism we use for daemon threads. The likelihood + # of this case is small because rarely will the _thread module be + # replaced by a module without _is_main_interpreter(). + # Furthermore, this is all irrelevant in applications + # that do not use subinterpreters. + def _is_main_interpreter(): + return True try: get_native_id = _thread.get_native_id _HAVE_THREAD_NATIVE_ID = True