diff --git a/Lib/concurrent/futures/__init__.py b/Lib/concurrent/futures/__init__.py index d6ac4b3e0b675f6..5d53a5ac50931d3 100644 --- a/Lib/concurrent/futures/__init__.py +++ b/Lib/concurrent/futures/__init__.py @@ -17,6 +17,9 @@ wait, as_completed) +lazy from .process import ProcessPoolExecutor +lazy from .thread import ThreadPoolExecutor + __all__ = [ 'FIRST_COMPLETED', 'FIRST_EXCEPTION', @@ -40,26 +43,9 @@ _interpreters = None if _interpreters: + lazy from .interpreter import InterpreterPoolExecutor # noqa: F401 __all__.append('InterpreterPoolExecutor') def __dir__(): return __all__ + ['__author__', '__doc__'] - - -def __getattr__(name): - global ProcessPoolExecutor, ThreadPoolExecutor, InterpreterPoolExecutor - - if name == 'ProcessPoolExecutor': - from .process import ProcessPoolExecutor - return ProcessPoolExecutor - - if name == 'ThreadPoolExecutor': - from .thread import ThreadPoolExecutor - return ThreadPoolExecutor - - if _interpreters and name == 'InterpreterPoolExecutor': - from .interpreter import InterpreterPoolExecutor - return InterpreterPoolExecutor - - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/test/test_concurrent_futures/test_shutdown.py b/Lib/test/test_concurrent_futures/test_shutdown.py index c576df1068ed320..129b5cb5057093f 100644 --- a/Lib/test/test_concurrent_futures/test_shutdown.py +++ b/Lib/test/test_concurrent_futures/test_shutdown.py @@ -115,20 +115,21 @@ def test_hang_gh83386(self): See https://github.com/python/cpython/issues/83386. """ - if self.executor_type == futures.ProcessPoolExecutor: - raise unittest.SkipTest( - "Hangs, see https://github.com/python/cpython/issues/83386") - rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import {executor_type} from test.test_concurrent_futures.test_shutdown import sleep_and_print if __name__ == "__main__": - if {context!r}: multiprocessing.set_start_method({context!r}) - t = {executor_type}(max_workers=3) + context = '{context}' + if context == "": + t = {executor_type}(max_workers=3) + else: + from multiprocessing import get_context + t = {executor_type}(max_workers=3, + mp_context=get_context(context)) t.submit(sleep_and_print, 1.0, "apple") t.shutdown(wait=False) """.format(executor_type=self.executor_type.__name__, - context=getattr(self, 'ctx', None))) + context=getattr(self, 'ctx', ''))) self.assertFalse(err) self.assertEqual(out.strip(), b"apple") @@ -243,8 +244,6 @@ def test_thread_names_default(self): t.join() def test_cancel_futures_wait_false(self): - # Can only be reliably tested for TPE, since PPE often hangs with - # `wait=False` (even without *cancel_futures*). rc, out, err = assert_python_ok('-c', """if True: from concurrent.futures import ThreadPoolExecutor from test.test_concurrent_futures.test_shutdown import sleep_and_print diff --git a/Lib/test/test_free_threading/test_sys.py b/Lib/test/test_free_threading/test_sys.py index 37b53bd723fd767..271fdd13c62b668 100644 --- a/Lib/test/test_free_threading/test_sys.py +++ b/Lib/test/test_free_threading/test_sys.py @@ -4,6 +4,27 @@ class SysModuleTest(unittest.TestCase): + @unittest.skipUnless(hasattr(sys, "setdlopenflags"), + "test needs sys.setdlopenflags()") + def test_dlopenflags_concurrent(self): + # gh-151644: getdlopenflags() and setdlopenflags() must be safe to + # call concurrently in free-threaded builds. + original = sys.getdlopenflags() + self.addCleanup(sys.setdlopenflags, original) + + # Use a small set of known-valid flag values to avoid integer overflow. + flag_values = [1, 2, 256, 257] + + def worker(worker_id): + for i in range(20_000): + if worker_id % 2 == 0: + sys.getdlopenflags() + else: + sys.setdlopenflags(flag_values[worker_id % len(flag_values)]) + + workers = [lambda i=i: worker(i) for i in range(6)] + threading_helper.run_concurrently(workers) + def test_int_max_str_digits_thread(self): # gh-151218: Check that it's safe to call get_int_max_str_digits() and # set_int_max_str_digits() in parallel. Previously, this test triggered diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst new file mode 100644 index 000000000000000..ff5058e14b42f15 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-18-00-00-00.gh-issue-151644.5cFffN.rst @@ -0,0 +1,4 @@ +Fix a data race in :func:`sys.setdlopenflags` and :func:`sys.getdlopenflags` +when called concurrently in the free-threaded build. The underlying +``_PyImport_GetDLOpenFlags`` and ``_PyImport_SetDLOpenFlags`` functions now +use atomic load/store operations. diff --git a/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst b/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst new file mode 100644 index 000000000000000..c3d56c6d59c91b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-03-14-54-33.gh-issue-152951.u8tPCI.rst @@ -0,0 +1,2 @@ +:class:`collections.deque` prevent rare crash when calling ``extend`` under +high memory pressure conditions. diff --git a/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst b/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst new file mode 100644 index 000000000000000..7aff384431edc77 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-03-17-29-34.gh-issue-150579.0tLpQukIU.rst @@ -0,0 +1,2 @@ +:mod:`concurrent.futures` now uses lazy imports for its executor submodules +instead of a module ``__getattr__`` hook. diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index e96a546a818d3d7..62d1dad5735ec8e 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -512,7 +512,6 @@ deque_extend_impl(dequeobject *deque, PyObject *iterable) iternext = *Py_TYPE(it)->tp_iternext; while ((item = iternext(it)) != NULL) { if (deque_append_lock_held(deque, item, maxlen) == -1) { - Py_DECREF(item); Py_DECREF(it); return NULL; } diff --git a/Python/import.c b/Python/import.c index 9b315b3125bd942..63e23e21beb1266 100644 --- a/Python/import.c +++ b/Python/import.c @@ -908,13 +908,13 @@ _PyImport_SwapPackageContext(const char *newcontext) int _PyImport_GetDLOpenFlags(PyInterpreterState *interp) { - return DLOPENFLAGS(interp); + return FT_ATOMIC_LOAD_INT_RELAXED(DLOPENFLAGS(interp)); } void _PyImport_SetDLOpenFlags(PyInterpreterState *interp, int new_val) { - DLOPENFLAGS(interp) = new_val; + FT_ATOMIC_STORE_INT_RELAXED(DLOPENFLAGS(interp), new_val); } #endif // HAVE_DLOPEN