https://github.com/python/cpython/commit/10b2d12431f871202668209fb03948e89c800b98
commit: 10b2d12431f871202668209fb03948e89c800b98
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-28T15:47:13Z
summary:

gh-157429: Make test_detach_materialized_dict_no_memory deterministic 
(GH-157444)

Add _testcapi.call_with_nomemory() which arms the allocation failure,
calls a function and removes the memory hooks without executing any
bytecode in between, so that pending work at the eval breaker (GC,
deferred frees on free-threading builds) cannot consume the failing
allocation.

The test now drops the last reference from C with this helper, accepts
only the unraisable exception reported while clearing the managed dict,
and no longer needs to run in a subprocess.

Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

files:
M Lib/test/test_class.py
M Modules/_testcapi/mem.c

diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py
index 3fdc87700f99adf..0ba3956be065a8d 100644
--- a/Lib/test/test_class.py
+++ b/Lib/test/test_class.py
@@ -2,7 +2,7 @@
 
 import unittest
 from test import support
-from test.support import cpython_only, import_helper, isolation
+from test.support import cpython_only, import_helper
 
 testmeths = [
 
@@ -1014,8 +1014,9 @@ class C:
         C.a = X()
 
     @support.nomemtest
-    @isolation.runInSubprocess()
     def test_detach_materialized_dict_no_memory(self):
+        import _testcapi
+
         class A:
             def __init__(self):
                 self.a = 1
@@ -1024,28 +1025,27 @@ def __init__(self):
         # The failing allocation should be the one which detaches the
         # dictionary from the object, but other allocations can happen
         # first, so try to fail every one of the first allocations.
-        raised = False
+        # Drop the last reference from C, so that nothing else (such as
+        # GC) runs between arming the failure and the deallocation.
+        seen = []
         for n in range(20):
-            a = A()
-            d = a.__dict__
-            try:
-                with support.catch_unraisable_exception() as ex:
-                    with support.inject_memory_error_cm(n, n + 1):
-                        del a
-                    exc_type = ex.unraisable and ex.unraisable.exc_type
-            except MemoryError:
-                # The failing allocation was not in the deallocation code.
-                continue
-            if exc_type is not MemoryError:
-                continue
-            raised = True
-            if "a" not in d:
-                # The dictionary was cleared, as expected.
+            lst = [A()]
+            d = lst[0].__dict__
+            with support.catch_unraisable_exception() as ex:
+                _testcapi.call_with_nomemory(n, n + 1, lst.clear)
+                if ex.unraisable is None:
+                    continue
+                exc_type = ex.unraisable.exc_type
+                err_msg = ex.unraisable.err_msg
+            seen.append((n, exc_type, err_msg))
+            if (exc_type is MemoryError and err_msg ==
+                    'Exception ignored while clearing an object managed dict'):
+                # The dictionary should have been cleared.
+                self.assertNotIn("a", d)
                 break
         else:
-            if not raised:
-                self.fail("MemoryError was not raised during deallocation")
-            self.fail("the dictionary was not cleared")
+            self.fail("MemoryError was not raised while detaching "
+                      f"the dictionary: {seen}")
 
 class DefinitionOrderTests(unittest.TestCase):
     # PEP 520: Preserving Class Attribute Definition Order
diff --git a/Modules/_testcapi/mem.c b/Modules/_testcapi/mem.c
index ba1462481231b50..257568172de8b19 100644
--- a/Modules/_testcapi/mem.c
+++ b/Modules/_testcapi/mem.c
@@ -210,6 +210,39 @@ remove_mem_hooks(PyObject *self, PyObject 
*Py_UNUSED(ignored))
     Py_RETURN_NONE;
 }
 
+static PyObject *
+call_with_nomemory(PyObject *self, PyObject *args)
+{
+    /* Call func(*args) with memory allocation failing as in set_nomemory().
+     * No bytecode is executed between arming the failure and the call. */
+    Py_ssize_t nargs = PyTuple_GET_SIZE(args);
+    if (nargs < 3) {
+        PyErr_SetString(PyExc_TypeError,
+                        "call_with_nomemory() requires at least 3 arguments");
+        return NULL;
+    }
+    int start = PyLong_AsInt(PyTuple_GET_ITEM(args, 0));
+    if (start == -1 && PyErr_Occurred()) {
+        return NULL;
+    }
+    int stop = PyLong_AsInt(PyTuple_GET_ITEM(args, 1));
+    if (stop == -1 && PyErr_Occurred()) {
+        return NULL;
+    }
+    PyObject *func = PyTuple_GET_ITEM(args, 2);
+    /* PyObject_Call() with a prebuilt tuple does not allocate,
+     * unlike PyObject_Vectorcall() for a callee without vectorcall. */
+    PyObject *callargs = PyTuple_GetSlice(args, 3, nargs);
+    if (callargs == NULL) {
+        return NULL;
+    }
+    fm_set_nomemory(start, stop);
+    PyObject *res = PyObject_Call(func, callargs, NULL);
+    fm_remove_hooks();
+    Py_DECREF(callargs);
+    return res;
+}
+
 static PyObject *
 test_setallocators(PyMemAllocatorDomain domain)
 {
@@ -966,6 +999,8 @@ static PyMethodDef test_methods[] = {
         PyDoc_STR("Remove memory hooks.")},
     {"set_nomemory",                  set_nomemory,                  
METH_VARARGS,
         PyDoc_STR("set_nomemory(start:int, stop:int = 0)")},
+    {"call_with_nomemory",            call_with_nomemory,            
METH_VARARGS,
+        PyDoc_STR("call_with_nomemory(start:int, stop:int, func, /, *args)")},
     {"test_pymem_alloc0",             test_pymem_alloc0,             
METH_NOARGS},
     {"test_pymem_setallocators",      test_pymem_setallocators,      
METH_NOARGS},
     {"test_pymem_setrawallocators",   test_pymem_setrawallocators,   
METH_NOARGS},

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to