From 60dff5a47b20a7efb6e43c571617b9139474bfe9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Aug 2026 15:09:47 +0300 Subject: [PATCH 1/8] gh-155997: Fix list_all() if an interpreter is destroyed during the call (GH-155998) Creating the Interpreter objects can start a garbage collection which finalizes an object owning the last reference to a listed interpreter. Skip interpreters which no longer exist instead of failing. --- Lib/concurrent/interpreters/__init__.py | 10 ++++++++-- Lib/test/test_interpreters/test_api.py | 15 +++++++++++++++ ...2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst | 3 +++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst diff --git a/Lib/concurrent/interpreters/__init__.py b/Lib/concurrent/interpreters/__init__.py index ea4147ee9a25da5..335a744b727d100 100644 --- a/Lib/concurrent/interpreters/__init__.py +++ b/Lib/concurrent/interpreters/__init__.py @@ -68,8 +68,14 @@ def create(): def list_all(): """Return all existing interpreters.""" - return [Interpreter(id, _whence=whence) - for id, whence in _interpreters.list_all(require_ready=True)] + interps = [] + for id, whence in _interpreters.list_all(require_ready=True): + try: + interps.append(Interpreter(id, _whence=whence)) + except InterpreterNotFoundError: + # It was destroyed after it was listed. + pass + return interps def get_current(): diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 13d23af5aceb475..aac3cdd717668ca 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -289,6 +289,21 @@ def test_idempotent(self): for interp1, interp2 in zip(actual, expected): self.assertIs(interp1, interp2) + def test_destroyed_by_gc(self): + # gh-155997: the interpreter is destroyed while list_all() runs. + interp = interpreters.create() + interpid = interp.id + cycle = [] + cycle.append(cycle) + cycle.append(interp) + # The cycle holds the only reference, so only the collector frees it. + with support.disable_gc(): + del interp, cycle + + with support.gc_threshold(1): + ids = [i.id for i in interpreters.list_all()] + self.assertNotIn(interpid, ids) + def test_created_with_capi(self): mainid, *_ = _interpreters.get_main() interpid1 = _interpreters.create() diff --git a/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst new file mode 100644 index 000000000000000..2727ba34aad8ff5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst @@ -0,0 +1,3 @@ +Fix :func:`concurrent.interpreters.list_all`. It failed if an interpreter +was destroyed during the call, in particular by a garbage collection which +finalized the object owning the last reference to it. From e675e37421357cf0319c5bca2cec533f0909d5b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Tue, 18 Aug 2026 20:31:52 +0800 Subject: [PATCH 2/8] gh-153578: Fix out-of-bounds write in bytearray.extend() with a reentrant __buffer__ (GH-153579) bytearray.extend() clamped only the high bound of the append range to the current size after acquiring the argument's buffer, so a __buffer__ that shrinks the bytearray left the low bound past the high bound and ran a negative-size memmove. Clamp the low bound too, matching bytearray.__iadd__. --- Lib/test/test_bytes.py | 24 +++++++++++++++++++ ...-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst | 3 +++ Objects/bytearrayobject.c | 3 +++ 3 files changed, 30 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 720b38cb508cbe6..1b9918c6c8f473c 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1828,6 +1828,30 @@ def test_setslice_trap(self): b[8:] = b self.assertEqual(b, bytearray(list(range(8)) + list(range(256)))) + def test_setslice_reentrant_resize(self): + # gh-153578: a buffer argument whose __buffer__ resizes the bytearray + # while the buffer is being acquired must not leave the slice bounds + # with lo > hi, which drove a negative-size memmove (an out-of-bounds + # write) in the setslice path reached through extend(). + class Evil: + def __init__(self, resize): + self.resize = resize + def __buffer__(self, flags): + self.resize() + return memoryview(b'ABCDEFGH') + # clear() during __buffer__: extend appends to the emptied bytearray. + b = bytearray(b'x' * 100) + b.extend(Evil(b.clear)) + self.assertEqual(b, b'ABCDEFGH') + # partial shrink during __buffer__. + b = bytearray(b'x' * 100) + b.extend(Evil(lambda: b.__delitem__(slice(30, None)))) + self.assertEqual(b, b'x' * 30 + b'ABCDEFGH') + # grow during __buffer__: the data lands at the original end. + b = bytearray(b'x' * 10) + b.extend(Evil(lambda: b.extend(b'y' * 100))) + self.assertEqual(b, b'x' * 10 + b'ABCDEFGH' + b'y' * 100) + def test_iconcat(self): b = bytearray(b"abc") b1 = b diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst new file mode 100644 index 000000000000000..2d5d4058a04ef2c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst @@ -0,0 +1,3 @@ +Fix an out-of-bounds write in :meth:`bytearray.extend` when the bytearray is +resized while the argument's :meth:`~object.__buffer__` is being acquired, for +example by another thread. Patch by tonghuaroot. diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index d009877dc09fac0..055fedc3ddfb034 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -681,8 +681,11 @@ bytearray_setslice(PyByteArrayObject *self, Py_ssize_t lo, Py_ssize_t hi, bytes = vbytes.buf; } + // gh-153578: __buffer__() may have resized self; re-clamp both bounds. if (lo < 0) lo = 0; + else if (lo > Py_SIZE(self)) + lo = Py_SIZE(self); if (hi < lo) hi = lo; if (hi > Py_SIZE(self)) From 915970ce9d031388a2bcf3e9f6199fa3e0eb9ebd Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Aug 2026 15:35:22 +0300 Subject: [PATCH 3/8] gh-113318: Fix @getter and @setter in Argument Clinic (GH-155778) Fix generating an accessor in a preprocessor conditional block. Reject the accessors of the same attribute with different C basenames and the same accessor defined twice. Reject deletion of the attribute, which crashed the setter, unless the new directive @deleter is applied to it. --- Lib/test/clinic.test.c | 49 +++++- Lib/test/test_clinic.py | 144 +++++++++++++++++- ...-08-14-12-46-07.gh-issue-113318.DYGQjo.rst | 5 + ...-08-14-12-45-57.gh-issue-113318.G1B0oH.rst | 6 + Modules/_asynciomodule.c | 16 -- Modules/_ctypes/_ctypes.c | 22 +-- Modules/_ctypes/clinic/_ctypes.c.h | 8 +- Modules/_io/clinic/textio.c.h | 8 +- Modules/_io/textio.c | 4 - Modules/_sqlite/clinic/cursor.c.h | 8 +- Modules/clinic/_asynciomodule.c.h | 26 +++- Modules/clinic/_ssl.c.h | 74 ++++++++- Objects/clinic/frameobject.c.h | 14 +- Objects/exceptions.c | 12 +- Objects/frameobject.c | 7 +- Objects/funcobject.c | 9 +- Python/traceback.c | 3 +- Tools/clinic/libclinic/clanguage.py | 9 +- Tools/clinic/libclinic/converters.py | 4 +- Tools/clinic/libclinic/dsl_parser.py | 59 ++++--- Tools/clinic/libclinic/function.py | 9 +- Tools/clinic/libclinic/parse_args.py | 32 +++- 22 files changed, 445 insertions(+), 83 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index 3dca8b8d1ed9b99..2ac153ac43e7029 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5431,6 +5431,12 @@ Test_property_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'property' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = Test_property_set_impl((TestObj *)self, value); return return_value; @@ -5438,7 +5444,40 @@ Test_property_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) static int Test_property_set_impl(TestObj *self, PyObject *value) -/*[clinic end generated code: output=49f925ab2a33b637 input=3bc3f46a23c83a88]*/ +/*[clinic end generated code: output=ec103a151cf51d25 input=3bc3f46a23c83a88]*/ + +/*[clinic input] +@setter +@deleter +Test.settable_and_deletable +[clinic start generated code]*/ + +#if !defined(Test_settable_and_deletable_DOCSTR) +# define Test_settable_and_deletable_DOCSTR NULL +#endif +#if defined(TEST_SETTABLE_AND_DELETABLE_GETSETDEF) +# undef TEST_SETTABLE_AND_DELETABLE_GETSETDEF +# define TEST_SETTABLE_AND_DELETABLE_GETSETDEF {"settable_and_deletable", (getter)Test_settable_and_deletable_get, (setter)Test_settable_and_deletable_set, Test_settable_and_deletable_DOCSTR}, +#else +# define TEST_SETTABLE_AND_DELETABLE_GETSETDEF {"settable_and_deletable", NULL, (setter)Test_settable_and_deletable_set, NULL}, +#endif + +static int +Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value); + +static int +Test_settable_and_deletable_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) +{ + int return_value; + + return_value = Test_settable_and_deletable_set_impl((TestObj *)self, value); + + return return_value; +} + +static int +Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value) +/*[clinic end generated code: output=479986d499b2f56d input=f5647f3511b9daea]*/ /*[clinic input] @setter @@ -5463,6 +5502,12 @@ Test_setter_first_with_docstr_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'setter_first_with_docstr' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = Test_setter_first_with_docstr_set_impl((TestObj *)self, value); return return_value; @@ -5470,7 +5515,7 @@ Test_setter_first_with_docstr_set(PyObject *self, PyObject *value, void *Py_UNUS static int Test_setter_first_with_docstr_set_impl(TestObj *self, PyObject *value) -/*[clinic end generated code: output=5aaf44373c0af545 input=31a045ce11bbe961]*/ +/*[clinic end generated code: output=eac8bafcaa50aa51 input=31a045ce11bbe961]*/ /*[clinic input] @getter diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 1dc1c4eaaaba196..f0dc62967f6a776 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -794,6 +794,102 @@ def test_ignore_preprocessor_in_comments(self): """) self.clinic.parse(raw) + def test_getset_in_ifdef(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + #ifdef CONDITION + /*[clinic input] + @getter + Foo.property + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property + [clinic start generated code]*/ + #endif + """ + generated = self.clinic.parse(dedent(block)) + self.assertIn("#if defined(CONDITION)", generated) + # The getset is undefined if the condition is false. + self.assertIn("#ifndef FOO_PROPERTY_GETSETDEF\n" + " #define FOO_PROPERTY_GETSETDEF\n" + "#endif /* !defined(FOO_PROPERTY_GETSETDEF) */", + generated) + + def test_getset_duplicate(self): + for annotation in "@getter", "@setter": + with self.subTest(annotation=annotation): + self.clinic = _make_clinic(filename="test.c") + block = f""" + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + {annotation} + Foo.property + [clinic start generated code]*/ + /*[clinic input] + {annotation} + Foo.property + [clinic start generated code]*/ + """ + kind = 'setter' if annotation == '@setter' else 'getter' + err = f"Cannot apply @{kind} to 'Foo.property' twice" + self.expect_failure(block, err, lineno=10) + + def test_getset_different_c_basename(self): + block = """ + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @getter + Foo.property as foo_get + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property as foo_set + [clinic start generated code]*/ + """ + err = "The accessors of 'Foo.property' must have the same C basename" + self.expect_failure(block, err, lineno=10) + + def test_setter_deletion_check(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + self.assertIn("if (value == NULL) {", generated) + self.assertIn("\"attribute 'property' of '%.100s' objects " + "cannot be deleted\"", generated) + + def test_deleter(self): + # @deleter means that the setter is called with NULL to delete + # the attribute, so it checks the value itself. + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + @deleter + Foo.property + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + self.assertNotIn("if (value == NULL) {", generated) + def test_var_keyword_non_dict(self): err = "'var_keyword_object' is not a valid converter" block = """ @@ -2671,7 +2767,7 @@ class Foo "" "" {annotation} Foo.property -> int """ - expected_error = f"{annotation} method cannot define a return type" + expected_error = "@getter and @setter methods cannot define a return type" self.expect_failure(block, expected_error, lineno=3) block = f""" @@ -2682,7 +2778,7 @@ class Foo "" "" obj: int / """ - expected_error = f"{annotation} methods cannot define parameters" + expected_error = "@getter and @setter methods cannot define parameters" self.expect_failure(block, expected_error) def test_setter_docstring(self): @@ -2725,9 +2821,51 @@ class Foo "" "" {dup[1]} Foo.property -> int """ - expected_error = "Cannot apply both @getter and @setter to the same function!" + expected_error = (f"Can't set {dup[1]}, " + f"function is not a normal callable") self.expect_failure(block, expected_error, lineno=3) + def test_deleter_without_setter(self): + block = """ + module foo + class Foo "" "" + @deleter + Foo.property + """ + expected_error = "Can't set @deleter, @setter is not applied" + self.expect_failure(block, expected_error, lineno=2) + + block = """ + module foo + class Foo "" "" + @deleter + @setter + Foo.property + """ + self.expect_failure(block, expected_error, lineno=2) + + def test_deleter_twice(self): + block = """ + module foo + class Foo "" "" + @setter + @deleter + @deleter + Foo.property + """ + expected_error = "Cannot apply @deleter twice to the same function!" + self.expect_failure(block, expected_error, lineno=4) + + def test_setter_and_deleter(self): + function = self.parse_function(""" + module foo + class Foo "" "" + @setter + @deleter + Foo.property + """, signatures_in_block=3, function_index=2) + self.assertEqual(function.kind, FunctionKind.SETTER_AND_DELETER) + def test_getset_no_class(self): for annotation in "@getter", "@setter": with self.subTest(annotation=annotation): diff --git a/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst b/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst new file mode 100644 index 000000000000000..4cd4acd01886368 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst @@ -0,0 +1,5 @@ +Fix crashes when deleting an attribute whose setter is generated by Argument +Clinic and is not prepared for deletion, among them +:attr:`frame.f_trace_opcodes` and the ``context``, ``owner`` and ``session`` +attributes of ``_ssl._SSLSocket``. +Deleting such attribute now raises :exc:`AttributeError`. diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst new file mode 100644 index 000000000000000..3ea0a37288fe880 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst @@ -0,0 +1,6 @@ +Fix Argument Clinic for ``@getter`` and ``@setter`` in a preprocessor +conditional block. +It failed with an internal error. +Argument Clinic now also rejects the accessors of the same attribute with +different C basenames, and the same accessor defined twice, which silently +generated invalid or duplicated entries of :c:type:`PyGetSetDef`. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 41384b388142ccc..a380f8ac72b32f4 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1384,10 +1384,6 @@ _asyncio_Future__asyncio_future_blocking_set_impl(FutureObj *self, if (future_ensure_alive(self)) { return -1; } - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { @@ -1427,10 +1423,6 @@ static int _asyncio_Future__log_traceback_set_impl(FutureObj *self, PyObject *value) /*[clinic end generated code: output=9ce8e19504f42f54 input=30ac8217754b08c2]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { return -1; @@ -1592,10 +1584,6 @@ static int _asyncio_Future__cancel_message_set_impl(FutureObj *self, PyObject *value) /*[clinic end generated code: output=0854b2f77bff2209 input=f461d17f2d891fad]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } Py_INCREF(value); Py_XSETREF(self->fut_cancel_msg, value); return 0; @@ -2450,10 +2438,6 @@ static int _asyncio_Task__log_destroy_pending_set_impl(TaskObj *self, PyObject *value) /*[clinic end generated code: output=7ebc030bb92ec5ce input=49b759c97d1216a4]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { return -1; diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index adfdf44e53604e3..034f26807f84aa8 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -598,13 +598,14 @@ _ctypes_CType_Type___pointer_type___get_impl(PyObject *self) /*[clinic input] @setter +@deleter _ctypes.CType_Type.__pointer_type__ [clinic start generated code]*/ static int _ctypes_CType_Type___pointer_type___set_impl(PyObject *self, PyObject *value) -/*[clinic end generated code: output=6259be8ea21693fa input=a05055fc7f4714b6]*/ +/*[clinic end generated code: output=6259be8ea21693fa input=7e24bceb1676349b]*/ { ctypes_state *st = get_module_state_by_def(Py_TYPE(self)); StgInfo *info; @@ -1491,10 +1492,6 @@ _ctypes_PyCArrayType_Type_raw_set_impl(CDataObject *self, PyObject *value) Py_ssize_t size; Py_buffer view; - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } if (PyObject_GetBuffer(value, &view, PyBUF_SIMPLE) < 0) return -1; size = view.len; @@ -1550,12 +1547,13 @@ _ctypes_PyCArrayType_Type_value_get_impl(CDataObject *self) /*[clinic input] @critical_section @setter +@deleter _ctypes.PyCArrayType_Type.value [clinic start generated code]*/ static int _ctypes_PyCArrayType_Type_value_set_impl(CDataObject *self, PyObject *value) -/*[clinic end generated code: output=39ad655636a28dd5 input=e2e6385fc6ab1a29]*/ +/*[clinic end generated code: output=39ad655636a28dd5 input=167f0935cbb8d489]*/ { const char *ptr; Py_ssize_t size; @@ -3664,12 +3662,13 @@ _validate_paramflags(ctypes_state *st, PyTypeObject *type, PyObject *paramflags, /*[clinic input] @critical_section @setter +@deleter _ctypes.CFuncPtr.errcheck [clinic start generated code]*/ static int _ctypes_CFuncPtr_errcheck_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=6580cf1ffdf3b9fb input=84930bb16c490b33]*/ +/*[clinic end generated code: output=6580cf1ffdf3b9fb input=bcd5d3ed1a0c36e9]*/ { if (value && !PyCallable_Check(value)) { PyErr_SetString(PyExc_TypeError, @@ -3701,13 +3700,14 @@ _ctypes_CFuncPtr_errcheck_get_impl(PyCFuncPtrObject *self) /*[clinic input] @setter +@deleter @critical_section _ctypes.CFuncPtr.restype [clinic start generated code]*/ static int _ctypes_CFuncPtr_restype_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=0be0a086abbabf18 input=683c3bef4562ccc6]*/ +/*[clinic end generated code: output=0be0a086abbabf18 input=ffc941a26dbb31f3]*/ { PyObject *checker; if (value == NULL) { @@ -3764,13 +3764,14 @@ _ctypes_CFuncPtr_restype_get_impl(PyCFuncPtrObject *self) /*[clinic input] @setter +@deleter @critical_section _ctypes.CFuncPtr.argtypes [clinic start generated code]*/ static int _ctypes_CFuncPtr_argtypes_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=596a36e2ae89d7d1 input=c4627573e980aa8b]*/ +/*[clinic end generated code: output=596a36e2ae89d7d1 input=fd012f1fd7cc35be]*/ { if (value == NULL || value == Py_None) { atomic_xsetref(&self->argtypes, NULL); @@ -5413,12 +5414,13 @@ class _ctypes.Simple "CDataObject *" "clinic_state()->Simple_Type" /*[clinic input] @critical_section @setter +@deleter _ctypes.Simple.value [clinic start generated code]*/ static int _ctypes_Simple_value_set_impl(CDataObject *self, PyObject *value) -/*[clinic end generated code: output=f267186118939863 input=977af9dc9e71e857]*/ +/*[clinic end generated code: output=f267186118939863 input=4e6c1143d17c2c3f]*/ { PyObject *result; diff --git a/Modules/_ctypes/clinic/_ctypes.c.h b/Modules/_ctypes/clinic/_ctypes.c.h index 529872f0f17ebe9..221b110cd15d2de 100644 --- a/Modules/_ctypes/clinic/_ctypes.c.h +++ b/Modules/_ctypes/clinic/_ctypes.c.h @@ -477,6 +477,12 @@ _ctypes_PyCArrayType_Type_raw_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'raw' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ctypes_PyCArrayType_Type_raw_set_impl((CDataObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -1052,4 +1058,4 @@ Simple_from_outparm(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py } return Simple_from_outparm_impl(self, cls); } -/*[clinic end generated code: output=22105663d71237ca input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b89feb50c654de3f input=a9049054013a1b77]*/ diff --git a/Modules/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h index 3c682cb2f271aef..10d0f1390cccbb4 100644 --- a/Modules/_io/clinic/textio.c.h +++ b/Modules/_io/clinic/textio.c.h @@ -1325,6 +1325,12 @@ _io_TextIOWrapper__CHUNK_SIZE_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_CHUNK_SIZE' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _io_TextIOWrapper__CHUNK_SIZE_set_impl((textio *)self, value); Py_END_CRITICAL_SECTION(); @@ -1356,4 +1362,4 @@ _io_TextIOWrapper_buffer_get(PyObject *self, void *Py_UNUSED(context)) return return_value; } -/*[clinic end generated code: output=e34c75e1d2a12084 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e93032a0691ff0e4 input=a9049054013a1b77]*/ diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 5b3d379e4e75543..5630f0309d98cff 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -3414,10 +3414,6 @@ _io_TextIOWrapper__CHUNK_SIZE_set_impl(textio *self, PyObject *value) { Py_ssize_t n; CHECK_ATTACHED_INT(self); - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } n = PyNumber_AsSsize_t(value, PyExc_ValueError); if (n == -1 && PyErr_Occurred()) return -1; diff --git a/Modules/_sqlite/clinic/cursor.c.h b/Modules/_sqlite/clinic/cursor.c.h index 3cad9f3aef5ecd5..689466b1c2b85a1 100644 --- a/Modules/_sqlite/clinic/cursor.c.h +++ b/Modules/_sqlite/clinic/cursor.c.h @@ -367,8 +367,14 @@ _sqlite3_Cursor_arraysize_set(PyObject *self, PyObject *value, void *Py_UNUSED(c { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'arraysize' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = _sqlite3_Cursor_arraysize_set_impl((pysqlite_Cursor *)self, value); return return_value; } -/*[clinic end generated code: output=a0e3ebba9e4d0ece input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e7b20358f8213fd7 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index 8bbef6b50231250..14cf5eebc5eec7a 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -585,6 +585,12 @@ _asyncio_Future__asyncio_future_blocking_set(PyObject *self, PyObject *value, vo { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_asyncio_future_blocking' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__asyncio_future_blocking_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -635,6 +641,12 @@ _asyncio_Future__log_traceback_set(PyObject *self, PyObject *value, void *Py_UNU { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_log_traceback' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__log_traceback_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -810,6 +822,12 @@ _asyncio_Future__cancel_message_set(PyObject *self, PyObject *value, void *Py_UN { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_cancel_message' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__cancel_message_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -1002,6 +1020,12 @@ _asyncio_Task__log_destroy_pending_set(PyObject *self, PyObject *value, void *Py { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_log_destroy_pending' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Task__log_destroy_pending_set_impl((TaskObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -2234,4 +2258,4 @@ _asyncio_future_discard_from_awaited_by(PyObject *module, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=22e74568ff49f81f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=46d50c477614b57e input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index e337ed2390a1fc4..62d52fc5f1aa5dd 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -386,6 +386,12 @@ _ssl__SSLSocket_context_set(PyObject *self, PyObject *value, void *Py_UNUSED(con { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'context' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_context_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -509,6 +515,12 @@ _ssl__SSLSocket_owner_set(PyObject *self, PyObject *value, void *Py_UNUSED(conte { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'owner' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_owner_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -914,6 +926,12 @@ _ssl__SSLSocket_session_set(PyObject *self, PyObject *value, void *Py_UNUSED(con { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'session' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_session_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -1342,6 +1360,12 @@ _ssl__SSLContext_verify_mode_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'verify_mode' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_verify_mode_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1392,6 +1416,12 @@ _ssl__SSLContext_verify_flags_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'verify_flags' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_verify_flags_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1443,6 +1473,12 @@ _ssl__SSLContext_minimum_version_set(PyObject *self, PyObject *value, void *Py_U { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'minimum_version' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_minimum_version_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1494,6 +1530,12 @@ _ssl__SSLContext_maximum_version_set(PyObject *self, PyObject *value, void *Py_U { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'maximum_version' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_maximum_version_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1551,6 +1593,12 @@ _ssl__SSLContext_num_tickets_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'num_tickets' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_num_tickets_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1633,6 +1681,12 @@ _ssl__SSLContext_options_set(PyObject *self, PyObject *value, void *Py_UNUSED(co { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'options' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_options_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1683,6 +1737,12 @@ _ssl__SSLContext__host_flags_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_host_flags' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext__host_flags_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1733,6 +1793,12 @@ _ssl__SSLContext_check_hostname_set(PyObject *self, PyObject *value, void *Py_UN { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'check_hostname' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_check_hostname_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -2265,6 +2331,12 @@ _ssl__SSLContext_sni_callback_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'sni_callback' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_sni_callback_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -3326,4 +3398,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=aef2e74b706c6106 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3a5bdd8db17e32b1 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/frameobject.c.h b/Objects/clinic/frameobject.c.h index 327896f4b97c684..7b8dab1e015a6b0 100644 --- a/Objects/clinic/frameobject.c.h +++ b/Objects/clinic/frameobject.c.h @@ -265,6 +265,12 @@ frame_trace_opcodes_set(PyObject *self, PyObject *value, void *Py_UNUSED(context { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'f_trace_opcodes' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = frame_trace_opcodes_set_impl((PyFrameObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -290,6 +296,12 @@ frame_lineno_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'f_lineno' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = frame_lineno_set_impl((PyFrameObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -433,4 +445,4 @@ frame___sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored)) return return_value; } -/*[clinic end generated code: output=74abf652547c0c11 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a42421e56faa7a80 input=a9049054013a1b77]*/ diff --git a/Objects/exceptions.c b/Objects/exceptions.c index fb546ad2673576d..cc3e03baa4c7dfe 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -346,12 +346,13 @@ BaseException_args_get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.args [clinic start generated code]*/ static int BaseException_args_set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=331137e11d8f9e80 input=2400047ea5970a84]*/ +/*[clinic end generated code: output=331137e11d8f9e80 input=177ad350c8b45219]*/ { PyObject *seq; if (value == NULL) { @@ -385,13 +386,14 @@ BaseException___traceback___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__traceback__ [clinic start generated code]*/ static int BaseException___traceback___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=a82c86d9f29f48f0 input=12676035676badad]*/ +/*[clinic end generated code: output=a82c86d9f29f48f0 input=53a1df586023d786]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted"); @@ -430,13 +432,14 @@ BaseException___context___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__context__ [clinic start generated code]*/ static int BaseException___context___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=b4cb52dcca1da3bd input=c0971adf47fa1858]*/ +/*[clinic end generated code: output=b4cb52dcca1da3bd input=fe79e7c0a0854004]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__context__ may not be deleted"); @@ -473,13 +476,14 @@ BaseException___cause___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__cause__ [clinic start generated code]*/ static int BaseException___cause___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=6161315398aaf541 input=e1b403c0bde3f62a]*/ +/*[clinic end generated code: output=6161315398aaf541 input=3fdd9a0d1674abc9]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted"); diff --git a/Objects/frameobject.c b/Objects/frameobject.c index c50cbeaada3c406..5889cdaf2aa1652 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -1651,10 +1651,6 @@ frame_lineno_set_impl(PyFrameObject *self, PyObject *value) /*[clinic end generated code: output=e64c86ff6be64292 input=36ed3c896b27fb91]*/ { PyCodeObject *code = _PyFrame_GetCode(self->f_frame); - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } /* f_lineno must be an integer. */ if (!PyLong_CheckExact(value)) { PyErr_SetString(PyExc_ValueError, @@ -1868,12 +1864,13 @@ frame_trace_get_impl(PyFrameObject *self) @permit_long_summary @critical_section @setter +@deleter frame.f_trace as frame_trace [clinic start generated code]*/ static int frame_trace_set_impl(PyFrameObject *self, PyObject *value) -/*[clinic end generated code: output=d6fe08335cf76ae4 input=e57380734815dac5]*/ +/*[clinic end generated code: output=d6fe08335cf76ae4 input=9fb7a5805196eae2]*/ { if (value == Py_None) { value = NULL; diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 0c1fab7f6d33a8a..0481adadf668f8d 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -926,12 +926,13 @@ function___annotate___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__annotate__ [clinic start generated code]*/ static int function___annotate___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=05b7dfc07ada66cd input=eb6225e358d97448]*/ +/*[clinic end generated code: output=05b7dfc07ada66cd input=4bcfad0bdcfec768]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, @@ -980,12 +981,13 @@ function___annotations___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__annotations__ [clinic start generated code]*/ static int function___annotations___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=a61795d4a95eede4 input=5302641f686f0463]*/ +/*[clinic end generated code: output=a61795d4a95eede4 input=71f6a58c00ac6745]*/ { if (value == Py_None) value = NULL; @@ -1025,12 +1027,13 @@ function___type_params___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__type_params__ [clinic start generated code]*/ static int function___type_params___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=038b4cda220e56fb input=3862fbd4db2b70e8]*/ +/*[clinic end generated code: output=038b4cda220e56fb input=c0e33abc5901a2f5]*/ { /* Not legal to del f.__type_params__ or to set it to anything * other than a tuple object. */ diff --git a/Python/traceback.c b/Python/traceback.c index 5bfa28f9c7dc8b6..fe6a465bc64cc94 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -176,12 +176,13 @@ tb_lineno_get(PyObject *op, void *Py_UNUSED(_)) /*[clinic input] @critical_section @setter +@deleter traceback.tb_next [clinic start generated code]*/ static int traceback_tb_next_set_impl(PyTracebackObject *self, PyObject *value) -/*[clinic end generated code: output=d4868cbc48f2adac input=ce66367f85e3c443]*/ +/*[clinic end generated code: output=d4868cbc48f2adac input=936201ff689c5700]*/ { if (!value) { PyErr_Format(PyExc_TypeError, "can't delete tb_next attribute"); diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index a8473dba0512460..ab77e7ad6603cdc 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -13,7 +13,8 @@ from libclinic.function import ( Module, Class, Function, Parameter, ParamTuple, permute_optional_groups, - GETTER, SETTER, METHOD_INIT) + GETTER, METHOD_INIT, + ACCESSORS, SETTERS) from libclinic.converters import self_converter from libclinic.parse_args import ParseArgsCodeGen if TYPE_CHECKING: @@ -478,12 +479,12 @@ def render_function( full_name = f.full_name template_dict = {'full_name': full_name} template_dict['name'] = f.displayname - if f.kind in {GETTER, SETTER}: + if f.kind in ACCESSORS: template_dict['getset_name'] = f.c_basename.upper() template_dict['getset_basename'] = f.c_basename if f.kind is GETTER: template_dict['c_basename'] = f.c_basename + "_get" - elif f.kind is SETTER: + else: template_dict['c_basename'] = f.c_basename + "_set" # Implicitly add the setter value parameter. data.impl_parameters.append("PyObject *value") @@ -498,7 +499,7 @@ def render_function( for converter in converters: converter.set_template_dict(template_dict) - if f.kind not in {SETTER, METHOD_INIT}: + if f.kind not in SETTERS | {METHOD_INIT}: f.return_converter.render(f, data) template_dict['impl_return_type'] = f.return_converter.type diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py index 76091a9eedc1bff..5539bd2e12e35f5 100644 --- a/Tools/clinic/libclinic/converters.py +++ b/Tools/clinic/libclinic/converters.py @@ -8,7 +8,7 @@ from libclinic.function import ( Function, Parameter, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, - GETTER, SETTER) + ACCESSORS) from libclinic.codegen import CRenderData, TemplateDict from libclinic.converter import ( CConverter, legacy_converters, add_legacy_c_converter) @@ -1124,7 +1124,7 @@ def correct_name_for_self( f: Function, parser: bool = False ) -> tuple[str, str]: - if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}: + if f.kind in {CALLABLE, METHOD_INIT} | ACCESSORS: if f.cls: return "PyObject *", "self" return "PyObject *", "module" diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 4dcbc815cc6f25b..a6b1d2bed5e5dee 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -18,7 +18,7 @@ Module, Class, Function, Parameter, FunctionKind, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, - GETTER, SETTER) + ACCESSORS, SETTERS) from libclinic.converter import ( converters, legacy_converters) from libclinic.converters import ( @@ -447,21 +447,31 @@ def at_disable(self, *args: str) -> None: def at_getter(self) -> None: match self.kind: + case FunctionKind.CALLABLE: + self.kind = FunctionKind.GETTER case FunctionKind.GETTER: fail("Cannot apply @getter twice to the same function!") - case FunctionKind.SETTER: - fail("Cannot apply both @getter and @setter to the same function!") case _: - self.kind = FunctionKind.GETTER + fail("Can't set @getter, function is not a normal callable") def at_setter(self) -> None: match self.kind: - case FunctionKind.SETTER: + case FunctionKind.CALLABLE: + self.kind = FunctionKind.SETTER + case FunctionKind.SETTER | FunctionKind.SETTER_AND_DELETER: fail("Cannot apply @setter twice to the same function!") - case FunctionKind.GETTER: - fail("Cannot apply both @getter and @setter to the same function!") case _: - self.kind = FunctionKind.SETTER + fail("Can't set @setter, function is not a normal callable") + + def at_deleter(self) -> None: + match self.kind: + case FunctionKind.SETTER: + # The setter is called with NULL to delete the attribute. + self.kind = FunctionKind.SETTER_AND_DELETER + case FunctionKind.SETTER_AND_DELETER: + fail("Cannot apply @deleter twice to the same function!") + case _: + fail("Can't set @deleter, @setter is not applied") def at_staticmethod(self) -> None: if self.kind is not CALLABLE: @@ -592,7 +602,7 @@ def normalize_function_kind(self, fullname: str) -> None: fail(f"{name!r} must be a normal method; got '{self.kind}'!") if name == '__new__' and (self.kind is not CLASS_METHOD or not cls): fail("'__new__' must be a class method!") - if self.kind in {GETTER, SETTER} and not cls: + if self.kind in ACCESSORS and not cls: fail("@getter and @setter must be methods") # Normalise self.kind. @@ -605,8 +615,8 @@ def resolve_return_converter( self, full_name: str, forced_converter: str ) -> CReturnConverter: if forced_converter: - if self.kind in {GETTER, SETTER}: - fail(f"@{self.kind.name.lower()} method cannot define a return type") + if self.kind in ACCESSORS: + fail("@getter and @setter methods cannot define a return type") if self.kind is METHOD_INIT: fail("__init__ methods cannot define a return type") ast_input = f"def x() -> {forced_converter}: pass" @@ -626,7 +636,7 @@ def resolve_return_converter( except ValueError: fail(f"Badly formed annotation for {full_name!r}: {forced_converter!r}") - if self.kind in {METHOD_INIT, SETTER}: + if self.kind in {METHOD_INIT} | SETTERS: return int_return_converter() return CReturnConverter() @@ -732,6 +742,22 @@ def state_modulename_name(self, line: str) -> None: self.next(self.state_parameters_start) def add_function(self, func: Function) -> None: + if func.kind in ACCESSORS: + # The accessors of the same attribute are rendered into a single + # PyGetSetDef entry, which is identified by the C basename, so + # they must share it. + for other in (func.cls or func.module).functions: + if (other.kind in ACCESSORS + and other.full_name == func.full_name): + if (other.kind is func.kind + or {other.kind, func.kind} <= SETTERS): + kind = 'setter' if func.kind in SETTERS else 'getter' + fail(f"Cannot apply @{kind} to " + f"{func.full_name!r} twice") + if other.c_basename != func.c_basename: + fail(f"The accessors of {func.full_name!r} " + f"must have the same C basename") + # Insert a self converter automatically. tp, name = correct_name_for_self(func) if func.cls and tp == "PyObject *": @@ -814,9 +840,8 @@ def state_parameters_start(self, line: str) -> None: return self.next(self.state_function_docstring, line) assert self.function is not None - if self.function.kind in {GETTER, SETTER}: - getset = self.function.kind.name.lower() - fail(f"@{getset} methods cannot define parameters") + if self.function.kind in ACCESSORS: + fail("@getter and @setter methods cannot define parameters") self.parameter_continuation = '' return self.next(self.state_parameter, line) @@ -1358,7 +1383,7 @@ def format_docstring_signature( lines.append(f.displayname) if f.forced_text_signature: lines.append(f.forced_text_signature) - elif f.kind in {GETTER, SETTER}: + elif f.kind in ACCESSORS: # @getter and @setter do not need signatures like a method or a function. return '' else: @@ -1541,7 +1566,7 @@ def format_docstring(self) -> str: assert self.function is not None f = self.function # For the following special cases, it does not make sense to render a docstring. - if f.kind in {METHOD_INIT, METHOD_NEW, GETTER, SETTER} and not f.docstring: + if f.kind in {METHOD_INIT, METHOD_NEW} | ACCESSORS and not f.docstring: return f.docstring # Enforce the summary line! diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index 325633eb010608f..cad673045d1c26d 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -60,6 +60,7 @@ class FunctionKind(enum.Enum): METHOD_NEW = enum.auto() GETTER = enum.auto() SETTER = enum.auto() + SETTER_AND_DELETER = enum.auto() @functools.cached_property def new_or_init(self) -> bool: @@ -76,6 +77,12 @@ def __repr__(self) -> str: METHOD_NEW: Final = FunctionKind.METHOD_NEW GETTER: Final = FunctionKind.GETTER SETTER: Final = FunctionKind.SETTER +SETTER_AND_DELETER: Final = FunctionKind.SETTER_AND_DELETER + +# The kinds which implement the setter of an entry of PyGetSetDef. +SETTERS: Final = frozenset({SETTER, SETTER_AND_DELETER}) +# The kinds which implement an entry of PyGetSetDef. +ACCESSORS: Final = SETTERS | {GETTER} @dc.dataclass(repr=False) @@ -161,7 +168,7 @@ def methoddef_flags(self) -> str | None: case FunctionKind.STATIC_METHOD: flags.append('METH_STATIC') case _ as kind: - acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER} + acceptable_kinds = {FunctionKind.CALLABLE} | ACCESSORS assert kind in acceptable_kinds, f"unknown kind: {kind!r}" if self.coexist: flags.append('METH_COEXIST') diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 0e99a89d74d7241..b08b949028205d2 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -5,7 +5,8 @@ from libclinic import fail, warn from libclinic.function import ( Function, Parameter, - GETTER, SETTER, METHOD_NEW) + GETTER, SETTER, METHOD_NEW, + ACCESSORS, SETTERS) from libclinic.converter import CConverter from libclinic.converters import ( defining_class_converter, object_converter, self_converter) @@ -188,6 +189,21 @@ def declare_parser( #define {methoddef_name} #endif /* !defined({methoddef_name}) */ """) +GETSETDEF_PROTOTYPE_IFNDEF: Final[str] = libclinic.normalize_snippet(""" + #ifndef {getset_name}_GETSETDEF + #define {getset_name}_GETSETDEF + #endif /* !defined({getset_name}_GETSETDEF) */ +""") +# The setter is called with NULL to delete the attribute. Unless @deleter is +# applied to it, deletion is rejected before the implementation is called. +SETTER_PREAMBLE: Final[str] = libclinic.normalize_snippet(""" + if (value == NULL) {{ + PyErr_Format(PyExc_AttributeError, + "attribute '{name}' of '%.100s' objects cannot be deleted", + Py_TYPE({self_name})->tp_name); + return -1; + }} +""", indent=4) class ParseArgsCodeGen: @@ -328,7 +344,7 @@ def select_prototypes(self) -> None: self.methoddef_define = GETTERDEF_PROTOTYPE_DEFINE if self.func.docstring: self.docstring_definition = GETSET_DOCSTRING_PROTOTYPE_STRVAR - elif self.func.kind is SETTER: + elif self.func.kind in SETTERS: if self.func.docstring: fail("docstrings are only supported for @getter, not @setter") self.return_value_declaration = "int {parser_retval};" @@ -387,9 +403,12 @@ def parse_no_args(self) -> None: if self.func.kind is GETTER: self.parser_prototype = PARSER_PROTOTYPE_GETTER parser_code = [] - elif self.func.kind is SETTER: + elif self.func.kind in SETTERS: self.parser_prototype = PARSER_PROTOTYPE_SETTER - parser_code = [] + if self.func.kind is SETTER: + parser_code = [SETTER_PREAMBLE] + else: + parser_code = [] elif not self.requires_defining_class: # no self.parameters, METH_NOARGS self.flags = "METH_NOARGS" @@ -921,7 +940,10 @@ def process_methoddef(self, clang: CLanguage) -> None: self.cpp_endif = "#endif /* " + conditional + " */" if self.methoddef_define and self.codegen.add_ifndef_symbol(self.func.full_name): - self.methoddef_ifndef = METHODDEF_PROTOTYPE_IFNDEF + if self.func.kind in ACCESSORS: + self.methoddef_ifndef = GETSETDEF_PROTOTYPE_IFNDEF + else: + self.methoddef_ifndef = METHODDEF_PROTOTYPE_IFNDEF def finalize(self, clang: CLanguage) -> None: # add ';' to the end of self.parser_prototype and self.impl_prototype From 66d7c891044915ccb7a12e0cc2c78991508e816e Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Aug 2026 15:43:54 +0300 Subject: [PATCH 4/8] gh-75876: Run bigmem tests in a subprocess (GH-155302) A test which really allocates the memory it asks for (that is, run with -M) now runs in a subprocess, so that the memory it uses and the address space it fragments are released when it ends. A dummy run stays in the process. The parent process watches the memory usage of the subprocess while waiting for it, so the separate watchdog process is no longer needed. Co-authored-by: Claude Opus 5 (1M context) --- Lib/test/_isolated_sample.py | 10 ++ Lib/test/memory_watchdog.py | 40 ------ Lib/test/support/__init__.py | 62 ++++----- Lib/test/support/isolation.py | 127 +++++++++++++----- Lib/test/test_support.py | 17 ++- ...6-08-18-10-16-44.gh-issue-75876.PEZplM.rst | 6 + 6 files changed, 153 insertions(+), 109 deletions(-) delete mode 100644 Lib/test/memory_watchdog.py create mode 100644 Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst diff --git a/Lib/test/_isolated_sample.py b/Lib/test/_isolated_sample.py index 5853b654fc28cb6..3aa58835c883677 100644 --- a/Lib/test/_isolated_sample.py +++ b/Lib/test/_isolated_sample.py @@ -10,6 +10,7 @@ import sys import time import unittest +from test import support from test.support import isolation # DurationSample sleeps this long in the subprocess; a parent-reported duration @@ -178,3 +179,12 @@ class TimeoutSample(unittest.TestCase): @isolation.runInSubprocess(timeout=TIMEOUT) def test_hang(self): time.sleep(TIMEOUT_HANG) + + +class BigmemSample(unittest.TestCase): + + @support.bigmemtest(size=1024, memuse=1) + def test_where_it_runs(self, size): + # A real run is isolated by bigmemtest() itself, a dummy run is not. + self.assertEqual(isolation.runningInSubprocess, + bool(support.real_max_memuse)) diff --git a/Lib/test/memory_watchdog.py b/Lib/test/memory_watchdog.py deleted file mode 100644 index 4a3f66e1f822bab..000000000000000 --- a/Lib/test/memory_watchdog.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Memory watchdog: periodically read the memory usage of the main test process -and print it out, until terminated.""" - - -import sys -import time -from test.libregrtest.utils import get_process_memory_usage - - -ONE_GIB = (1024 ** 3) - - -def watchdog(pid): - while True: - mem = get_process_memory_usage(pid) - if mem is None: - # get_process_memory_usage() is not supported on the platform, - # or something went wrong. Exit since the next call is likely to - # fail the same way. - return - - # Prefer sys.stdout.write() to print() to use a single write() syscall. - # print(msg) calls write(msg.encode()) and then write(b"\n"). - sys.stdout.write(f" ... process data size: {mem / ONE_GIB:.1f} GiB\n") - sys.stdout.flush() - time.sleep(1) - -def main(): - if len(sys.argv) != 2: - print(f"usage: python {sys.argv[0]} pid") - sys.exit(1) - pid = int(sys.argv[1]) - - try: - watchdog(pid) - except KeyboardInterrupt: - pass - -if __name__ == "__main__": - main() diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index da116355eeea154..31e5508dd9b7907 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1270,26 +1270,17 @@ def set_memlimit(limit: str) -> None: max_memuse = memlimit -class _MemoryWatchdog: - """An object which periodically watches the process' memory consumption - and prints it out. - """ - - def __init__(self): - self.started = False +def _memory_watchdog(pid): + """Return a function printing the memory usage of process *pid*.""" + # Imported here: test.support does not depend on test.libregrtest. + from test.libregrtest.utils import get_process_memory_usage - def start(self): - import subprocess - watchdog_script = findfile("memory_watchdog.py") - cmd = [sys.executable, watchdog_script, str(os.getpid())] - self.mem_watchdog = subprocess.Popen(cmd) - self.started = True - - def stop(self): - if not self.started: - return - self.mem_watchdog.terminate() - self.mem_watchdog.wait() + def watch(): + mem = get_process_memory_usage(pid) + if mem is not None: + print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB", + flush=True) + return watch def bigmemtest(size, memuse, dry_run=True): @@ -1304,8 +1295,14 @@ def bigmemtest(size, memuse, dry_run=True): extra argument. If 'dry_run' is true, the value passed to the test method may be less than the requested value. If 'dry_run' is false, it means the test doesn't support dummy runs when -M is not specified. + + A test that actually allocates the requested memory (that is, one run with + -M) runs in a subprocess, so that the memory it uses and the address space + it fragments are released when it ends. A dummy run stays in the process. """ def decorator(f): + from test.support import isolation + @functools.wraps(f) def wrapper(self): size = wrapper.size @@ -1321,20 +1318,25 @@ def wrapper(self): "not enough memory: %.1fG minimum needed" % (size * memuse / (1024 ** 3))) - if real_max_memuse and verbose: + if (real_max_memuse and verbose + and not isolation.runningInSubprocess): print() peak = (size * memuse) / (1024 ** 3) - print(f" ... expected peak memory use: {peak:.1f} GiB") - watchdog = _MemoryWatchdog() - watchdog.start() - else: - watchdog = None + # Flushed, so that it precedes the memory usage below. + print(f" ... expected peak memory use: {peak:.1f} GiB", + flush=True) + + if (real_max_memuse and has_subprocess_support + and not isolation.runningInSubprocess): + # Watch it from here: the output of the subprocess is captured. + cls = type(self) + qualname = f'{cls.__qualname__}.{f.__name__}' + proc = isolation._start_test(cls.__module__, qualname) + watchdog = _memory_watchdog(proc.pid) if verbose else None + isolation._replay_test(self, *proc.wait(tick=watchdog)) + return - try: - return f(self, maxsize) - finally: - if watchdog: - watchdog.stop() + return f(self, maxsize) wrapper.size = size wrapper.memuse = memuse diff --git a/Lib/test/support/isolation.py b/Lib/test/support/isolation.py index bb4fa6b003cc20c..3cfd406b0f2f0e1 100644 --- a/Lib/test/support/isolation.py +++ b/Lib/test/support/isolation.py @@ -108,12 +108,75 @@ def _child_environ(env): return environ -def _run_in_subprocess(module, qualname, options, env, timeout): - """Run module.qualname (a test method or class) in a fresh subprocess. +class _SubprocessTest: + """A test running in a subprocess, started by _start_test(). - Return ``(payload, output, returncode)``, where *payload* is the decoded - ``{'outcomes': ..., 'durations': ...}`` mapping from the subprocess, or - ``None`` if it did not run to completion (crash, import error, ...). + The parent can watch the subprocess (its pid) while the test runs, and + must wait() for it. + """ + + def __init__(self, proc, result_path): + self._proc = proc + self._result_path = result_path + + @property + def pid(self): + return self._proc.pid + + def wait(self, timeout=None, tick=None, interval=1.0): + """Wait for the test to finish, calling *tick* every *interval* seconds. + + Return ``(payload, output, returncode)``, where *payload* is the + decoded ``{'outcomes': ..., 'durations': ...}`` mapping from the + subprocess, or ``None`` if it did not run to completion (crash, + import error, ...). + """ + import marshal + import subprocess + import time + deadline = None if timeout is None else time.monotonic() + timeout + try: + while True: + step = None if deadline is None else max( + 0.0, deadline - time.monotonic()) + # Wake up for the next tick, unless the timeout comes first. + ticking = tick is not None and (step is None or step > interval) + try: + # communicate(), not wait(): a test writing more than a + # pipe buffer would block. Retrying keeps what it read. + stdout, stderr = self._proc.communicate( + timeout=interval if ticking else step) + break + except subprocess.TimeoutExpired: + if ticking: + tick() + continue + # Report the hang rather than leaving the runner stuck. + self._proc.kill() + stdout, stderr = self._proc.communicate() + raise _SubprocessTestError( + f'test did not complete in a subprocess ' + f'within {timeout} seconds' + ) from _remote(_decode(stdout) + _decode(stderr)) + try: + with open(self._result_path, 'rb') as f: + payload = marshal.load(f) + except (OSError, EOFError, ValueError): + payload = None + output = _decode(stdout) + _decode(stderr) + return payload, output, self._proc.returncode + finally: + try: + os.unlink(self._result_path) + except OSError: + pass + + +def _start_test(module, qualname, options=(), env=None): + """Start module.qualname (a test method or class) in a fresh subprocess. + + Return a _SubprocessTest. Its wait() is what removes the temporary file + the subprocess writes its result to. """ import marshal import subprocess @@ -129,26 +192,16 @@ def _run_in_subprocess(module, qualname, options, env, timeout): cmd = [sys.executable, *options, '-m', 'test.support.subprocess_runner', module, qualname, result_path, marshal.dumps(_child_config()).hex()] - try: - proc = subprocess.run(cmd, capture_output=True, - env=_child_environ(env), timeout=timeout) - except subprocess.TimeoutExpired as exc: - # Report the hang rather than leaving the test runner stuck. - output = _decode(exc.stdout) + _decode(exc.stderr) - raise _SubprocessTestError( - f'test did not complete in a subprocess ' - f'within {timeout} seconds') from _remote(output) - try: - with open(result_path, 'rb') as f: - payload = marshal.load(f) - except (OSError, EOFError, ValueError): - payload = None - finally: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=_child_environ(env)) + except BaseException: try: os.unlink(result_path) except OSError: pass - return payload, _decode(proc.stdout) + _decode(proc.stderr), proc.returncode + raise + return _SubprocessTest(proc, result_path) + def _replay_outcome(test, outcome): @@ -200,6 +253,19 @@ def _check_returncode(returncode, output, what): raise exc from _remote(output) +def _replay_test(test, payload, output, returncode): + """Reproduce in *test* the result that _SubprocessTest.wait() returned.""" + if payload is None: + exc = _SubprocessTestError( + f'test did not complete in a subprocess (exit code {returncode})') + raise exc from _remote(output) + # The parent measures the test method's own duration (the real cost of the + # isolated run, subprocess startup included), so nothing to forward here. + # Replay the outcomes first: a failure of the test itself is more useful. + _replay_outcomes(test, payload['outcomes']) + _check_returncode(returncode, output, 'test') + + def _isolate_method(func, options, env, timeout): @functools.wraps(func) def wrapper(self, /, *args, **kwargs): @@ -209,18 +275,8 @@ def wrapper(self, /, *args, **kwargs): _check_subprocess_support() cls = type(self) qualname = f'{cls.__qualname__}.{func.__name__}' - payload, output, returncode = _run_in_subprocess(cls.__module__, - qualname, options, - env, timeout) - if payload is None: - exc = _SubprocessTestError( - f'test did not complete in a subprocess (exit code {returncode})') - raise exc from _remote(output) - # The parent measures this method's own duration (the real cost of the - # isolated run, subprocess startup included), so nothing to forward here. - # Replay the outcomes first: a failure of the test itself is more useful. - _replay_outcomes(self, payload['outcomes']) - _check_returncode(returncode, output, 'test') + proc = _start_test(cls.__module__, qualname, options, env) + _replay_test(self, *proc.wait(timeout)) return wrapper @@ -244,9 +300,8 @@ def setUpClass(cls): _check_subprocess_support() # Run the whole class in a single subprocess and stash the outcomes # for the test methods to replay. - payload, output, returncode = _run_in_subprocess(cls.__module__, - cls.__qualname__, - options, env, timeout) + proc = _start_test(cls.__module__, cls.__qualname__, options, env) + payload, output, returncode = proc.wait(timeout) if payload is None: exc = _SubprocessTestError( f'class did not complete in a subprocess (exit code {returncode})') diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 84b91bb00cdbe4d..243da190e48f5d2 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -1231,17 +1231,28 @@ def test_timeout_reported_as_error(self): self.assertEqual(len(result.errors), 1) self.assertIn(f'within {TIMEOUT} seconds', result.errors[0][1]) + @support.requires_subprocess() + def test_bigmemtest_isolates_a_real_run(self): + # A dummy run (no -M) stays in this process, a real run does not. + for memlimit in (0, support._1G): + with self.subTest(real_max_memuse=memlimit): + with support.swap_attr(support, 'real_max_memuse', memlimit): + result = self._run('BigmemSample') + self.assertEqual(result.testsRun, 1) + self.assertEqual(self._names(result.failures), []) + self.assertEqual(self._names(result.errors), []) + def test_skipped_without_subprocess_support(self): # On a platform without subprocess support the test is skipped in the # parent, before any subprocess is spawned. calls = [] - orig = isolation._run_in_subprocess + orig = isolation._start_test with support.swap_attr(support, 'has_subprocess_support', False): - isolation._run_in_subprocess = lambda *a, **k: calls.append(a) + isolation._start_test = lambda *a, **k: calls.append(a) try: result = self._run('MethodSample.test_pass') finally: - isolation._run_in_subprocess = orig + isolation._start_test = orig self.assertEqual(result.testsRun, 1) self.assertEqual(len(result.skipped), 1) self.assertEqual(calls, []) diff --git a/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst b/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst new file mode 100644 index 000000000000000..ee3da3730932f6e --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst @@ -0,0 +1,6 @@ +A test decorated with :func:`~test.support.bigmemtest` now runs in a +subprocess if it really allocates the memory it asks for (that is, if the +``-M`` option is used), so that the memory it uses and the address space it +fragments are released when it ends. A dummy run stays in the process. The +separate memory watchdog process is no longer needed: the parent process +watches the memory usage of the subprocess. From 05ab13e1019a8d734ceceb28dffc83619bf8880c Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:22:43 +0300 Subject: [PATCH 5/8] gh-155894: Fix asyncio.wait_for() docs claiming a coroutine is wrapped in a Task (#155895) --- Doc/library/asyncio-task.rst | 12 ++++++------ Lib/asyncio/tasks.py | 10 ++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 596cb7565a66e7d..c6f0fb8a50917ac 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -843,17 +843,13 @@ Timeouts Wait for the *fut* :ref:`awaitable ` to complete with a timeout. - If *fut* is a coroutine it is automatically scheduled as a Task. - *timeout* can either be ``None`` or a float or int number of seconds to wait for. If *timeout* is ``None``, block until the future completes. - If a timeout occurs, it cancels the task and raises - :exc:`TimeoutError`. + If a timeout occurs, it cancels *fut* and raises :exc:`TimeoutError`. - To avoid the task :meth:`cancellation `, - wrap it in :func:`shield`. + To prevent *fut* from being cancelled, wrap it in :func:`shield`. The function will wait until the future is actually cancelled, so the total wait time may exceed the *timeout*. If an exception @@ -894,6 +890,10 @@ Timeouts .. versionchanged:: 3.11 Raises :exc:`TimeoutError` instead of :exc:`asyncio.TimeoutError`. + .. versionchanged:: 3.12 + Implemented using :func:`asyncio.timeout`, a coroutine passed as *fut* + is no longer wrapped in a :class:`Task` when *timeout* is positive. + Waiting primitives ================== diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 7889d4793a5dec3..498eec3f31b292b 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -440,15 +440,13 @@ def _release_waiter(waiter, *args): async def wait_for(fut, timeout): """Wait for the single Future or coroutine to complete, with timeout. - Coroutine will be wrapped in Task. - Returns result of the Future or coroutine. When a timeout occurs, - it cancels the task and raises TimeoutError. To avoid the task - cancellation, wrap it in shield(). + it cancels fut and raises TimeoutError. To prevent fut from being + cancelled, wrap it in shield(). - If the wait is cancelled, the task is also cancelled. + If the wait is cancelled, fut is also cancelled. - If the task suppresses the cancellation and returns a value instead, + If fut suppresses the cancellation and returns a value instead, that value is returned. This function is a coroutine. From 95e32ba8262b2f08d4c574bce55425589cbf9885 Mon Sep 17 00:00:00 2001 From: Seungki Kim <78344167+danielKim614@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:36:16 +0900 Subject: [PATCH 6/8] gh-155941: Close the transport when client_connected_cb raises in asyncio (#155942) --- Lib/asyncio/streams.py | 12 ++++++- Lib/test/test_asyncio/test_streams.py | 32 +++++++++++++++++++ ...-08-17-21-00-00.gh-issue-155941.strmCb.rst | 4 +++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index f5c4f0b0c3297ba..954e132617ce42b 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -239,7 +239,17 @@ def connection_made(self, transport): self._over_ssl = transport.get_extra_info('sslcontext') is not None if self._client_connected_cb is not None: writer = StreamWriter(transport, self, reader, self._loop) - res = self._client_connected_cb(reader, writer) + try: + res = self._client_connected_cb(reader, writer) + except Exception as exc: + self._loop.call_exception_handler({ + 'message': 'Unhandled exception in client_connected_cb', + 'exception': exc, + 'transport': transport, + }) + transport.close() + self._strong_reader = None + return if coroutines.iscoroutine(res): def callback(task): if task.cancelled(): diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index 911087a128f9713..172f183849c3057 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -1267,6 +1267,38 @@ async def handle_echo(reader, writer): messages = self._basetest_unhandled_exceptions(handle_echo) self.assertEqual(messages, []) + def test_unhandled_exception_sync_callback(self): + # An exception raised by a plain-function client_connected_cb is + # reported like the coroutine case and the transport is closed. + port = socket_helper.find_unused_port() + + messages = [] + self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx)) + + async def client(): + rd, wr = await asyncio.open_connection('localhost', port) + async with asyncio.timeout(60): + data = await rd.read() + self.assertEqual(data, b'') # the server closed the connection + wr.close() + await wr.wait_closed() + + async def main(): + def handle_echo(reader, writer): + raise Exception('test') + + server = await asyncio.start_server( + handle_echo, 'localhost', port) + await server.start_serving() + await client() + server.close() + await server.wait_closed() + + self.loop.run_until_complete(main()) + + self.assertEqual(messages[0]['message'], + 'Unhandled exception in client_connected_cb') + def test_open_connection_happy_eyeball_refcycles(self): port = socket_helper.find_unused_port() async def main(): diff --git a/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst b/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst new file mode 100644 index 000000000000000..6cee7eb4eb9090e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst @@ -0,0 +1,4 @@ +Fix :func:`asyncio.start_server` when a plain-function *client_connected_cb* +raises: the error is now reported like the coroutine case and the transport +is closed, instead of leaving the connection open forever (which also made +:meth:`asyncio.Server.wait_closed` hang). From c612fd4cf363cf1af28bb67f11b4a11dca57507f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 18 Aug 2026 19:07:19 +0500 Subject: [PATCH 7/8] PC/pyconfig.h: Improve readability of implicit .lib linking (GH-155995) --- PC/pyconfig.h | 60 +++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 2381ed3772b109e..22c3ccc05bb004e 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -317,39 +317,33 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* All windows compilers that use this header support __declspec */ #define HAVE_DECLSPEC_DLL -/* For an MSVC DLL, we can nominate the .lib files used by extensions */ -#ifdef MS_COREDLL -# if !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_BUILTIN) - /* not building the core - must be an ext */ -# if defined(_MSC_VER) && !defined(Py_NO_LINK_LIB) - /* So MSVC users need not specify the .lib - file in their Makefile */ - /* Define Py_NO_LINK_LIB to build extension disabling pragma - based auto-linking. - This is relevant when using build-system generator (e.g CMake) where - the linking is explicitly handled */ -# if defined(Py_GIL_DISABLED) -# if defined(Py_DEBUG) -# pragma comment(lib,"python316t_d.lib") -# elif defined(Py_LIMITED_API) || defined(Py_TARGET_ABI3T) -# pragma comment(lib,"python3t.lib") -# else -# pragma comment(lib,"python316t.lib") -# endif /* Py_DEBUG */ -# else /* Py_GIL_DISABLED */ -# if defined(Py_DEBUG) -# pragma comment(lib,"python316_d.lib") -# elif defined(Py_TARGET_ABI3T) -# pragma comment(lib,"python3t.lib") -# elif defined(Py_LIMITED_API) -# pragma comment(lib,"python3.lib") -# else -# pragma comment(lib,"python316.lib") -# endif /* Py_DEBUG */ -# endif /* Py_GIL_DISABLED */ -# endif /* _MSC_VER && !Py_NO_LINK_LIB */ -# endif /* Py_BUILD_CORE */ -#endif /* MS_COREDLL */ +/* Automatic linking of extension python3x.lib files for MSVC DLLs. + This lets MSVC users build extensions without manually specifying .lib files. + Define Py_NO_LINK_LIB to disable this behavior. */ +#if !defined(Py_NO_LINK_LIB) \ + && defined(_MSC_VER) && defined(Py_ENABLE_SHARED) \ + && !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_BUILTIN) + /* not building the core - must be an ext */ +# if defined(Py_GIL_DISABLED) +# if defined(Py_DEBUG) +# pragma comment(lib,"python316t_d.lib") +# elif defined(Py_LIMITED_API) || defined(Py_TARGET_ABI3T) +# pragma comment(lib,"python3t.lib") +# else +# pragma comment(lib,"python316t.lib") +# endif /* Py_DEBUG */ +# else +# if defined(Py_DEBUG) +# pragma comment(lib,"python316_d.lib") +# elif defined(Py_TARGET_ABI3T) +# pragma comment(lib,"python3t.lib") +# elif defined(Py_LIMITED_API) +# pragma comment(lib,"python3.lib") +# else +# pragma comment(lib,"python316.lib") +# endif /* Py_DEBUG */ +# endif /* Py_GIL_DISABLED */ +#endif #ifdef MS_WIN64 /* maintain "win32" sys.platform for backward compatibility of Python code, From c5168eadf15665310b22ff44e23738034d3e5036 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Aug 2026 19:55:02 +0300 Subject: [PATCH 8/8] gh-64595: Touch the source file if the generated file was changed (GH-155265) The build system does not know that the source file depends on the files generated from it, so it did not recompile the source file if only the generated file was changed. The generated files are also kept newer than the source file. --- Lib/test/test_clinic.py | 58 +++++++++++++++++++ ...6-08-06-09-43-16.gh-issue-64595.W75XZK.rst | 5 ++ Tools/clinic/libclinic/cli.py | 7 ++- Tools/clinic/libclinic/utils.py | 45 +++++++++++--- 4 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index f0dc62967f6a776..94a69b6d7309df8 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -3214,6 +3214,64 @@ def test_no_change(self): # if the content does not change self.assertEqual(pre_mtime, post_mtime) + TOUCH_CODE = dedent(""" + /*[clinic input] + module m + [clinic start generated code]*/ + + /*[clinic input] + output everything file + m.func + a: int + / + + Docstring. + [clinic start generated code]*/ + """) + + def test_touch_source(self): + # gh-64595: The build system does not know that the source file + # depends on the file generated from it, so the modification + # times are updated to force the recompilation. + def mtimes(): + return os.stat(fn).st_mtime_ns, os.stat(dest).st_mtime_ns + + def set_mtimes(source, generated): + os.utime(fn, ns=(source, source)) + os.utime(dest, ns=(generated, generated)) + + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write(self.TOUCH_CODE) + dest = self.dest_file(fn) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreaterEqual(generated_mtime, source_mtime) + + # The generated file is changed, so both files are touched. + os.unlink(dest) + old = source_mtime - 10**10 + os.utime(fn, ns=(old, old)) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreater(source_mtime, old) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed, but the source file is newer, so only + # the generated file is touched. + set_mtimes(source_mtime - 10**10, source_mtime - 2 * 10**10) + old_source_mtime = os.stat(fn).st_mtime_ns + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertEqual(source_mtime, old_source_mtime) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed and the generated file is newer, + # so no file is touched. + self.expect_success(fn) + self.assertEqual(mtimes(), (source_mtime, generated_mtime)) + def test_cli_force(self): invalid_input = dedent(""" /*[clinic input] diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst new file mode 100644 index 000000000000000..6c9393347d31ce5 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst @@ -0,0 +1,5 @@ +Argument Clinic now touches the source file if a file generated from it was +changed, and keeps the generated files newer than the source file. +The build system does not know that the source file depends on the files +generated from it, so it did not recompile the source file if only the +generated file was changed. diff --git a/Tools/clinic/libclinic/cli.py b/Tools/clinic/libclinic/cli.py index c66084cf3144826..290fc3a6e59408a 100644 --- a/Tools/clinic/libclinic/cli.py +++ b/Tools/clinic/libclinic/cli.py @@ -89,10 +89,15 @@ def parse_file( filename=filename, limited_capi=limited_capi, writer=writer) + index = len(writer.files) cooked = clinic.parse(raw) - writer.write(output, cooked) + files = writer.files[index:] + writer.update_times(output, + [fn for fn, _ in files if fn != output], + any(changed for _, changed in files)) + def create_cli() -> argparse.ArgumentParser: cmdline = argparse.ArgumentParser( diff --git a/Tools/clinic/libclinic/utils.py b/Tools/clinic/libclinic/utils.py index 8fc8748f0f9ae10..01015ff1237656b 100644 --- a/Tools/clinic/libclinic/utils.py +++ b/Tools/clinic/libclinic/utils.py @@ -5,6 +5,7 @@ import os import re import string +from collections.abc import Iterable from typing import Literal, Final @@ -17,11 +18,14 @@ def read_file(filename: str) -> str | None: return None -def write_file(filename: str, new_contents: str) -> None: - """Write new content to file, iff the content changed.""" +def write_file(filename: str, new_contents: str) -> bool: + """Write new content to file, iff the content changed. + + Return True if the file was written. + """ if read_file(filename) == new_contents: # no change: avoid modifying the file modification time - return + return False # Atomic write using a temporary file and os.replace() filename_new = f"{filename}.new" with open(filename_new, "w", encoding="utf-8") as fp: @@ -31,6 +35,7 @@ def write_file(filename: str, new_contents: str) -> None: except: os.unlink(filename_new) raise + return True @dc.dataclass(slots=True, frozen=True) @@ -50,6 +55,8 @@ class FileWriter: dry_run: bool = False changes: list[FileChange] = dc.field(default_factory=list) + # (filename, changed) for every file which was passed to write(). + files: list[tuple[str, bool]] = dc.field(default_factory=list) def makedirs(self, dirname: str) -> None: if not self.dry_run: @@ -61,12 +68,34 @@ def makedirs(self, dirname: str) -> None: def write(self, filename: str, new_contents: str) -> None: if not self.dry_run: - write_file(filename, new_contents) + changed = write_file(filename, new_contents) + else: + old_contents = read_file(filename) + changed = old_contents != new_contents + if changed: + self.changes.append( + FileChange(filename, old_contents, new_contents)) + self.files.append((filename, changed)) + + def update_times(self, source: str, generated: Iterable[str], + changed: bool) -> None: + """Keep the generated files newer than the source file. + + The build system does not know that the source file depends on + the files generated from it, so the source file is touched to + force its recompilation. + """ + if self.dry_run: return - old_contents = read_file(filename) - if old_contents != new_contents: - self.changes.append( - FileChange(filename, old_contents, new_contents)) + if changed: + os.utime(source) + for filename in generated: + os.utime(filename) + else: + mtime = os.stat(source).st_mtime_ns + for filename in generated: + if os.stat(filename).st_mtime_ns <= mtime: + os.utime(filename) def compute_checksum(input_: str, length: int | None = None) -> str: