https://github.com/python/cpython/commit/90a1f02b7da0c356d10a23f1faee7dc063f887ed
commit: 90a1f02b7da0c356d10a23f1faee7dc063f887ed
branch: 3.14
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-18T13:09:24Z
summary:

[3.14] gh-113318: Fix @getter and @setter in Argument Clinic (GH-155778) 
(GH-156011)

(cherry picked from commit 915970ce9d031388a2bcf3e9f6199fa3e0eb9ebd)

files:
A Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst
A Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst
M Lib/test/clinic.test.c
M Lib/test/test_clinic.py
M Modules/_asynciomodule.c
M Modules/_ctypes/_ctypes.c
M Modules/_ctypes/clinic/_ctypes.c.h
M Modules/_io/clinic/textio.c.h
M Modules/_io/textio.c
M Modules/_sqlite/clinic/cursor.c.h
M Modules/clinic/_asynciomodule.c.h
M Modules/clinic/_ssl.c.h
M Objects/clinic/frameobject.c.h
M Objects/exceptions.c
M Objects/frameobject.c
M Objects/funcobject.c
M Python/traceback.c
M Tools/clinic/libclinic/clanguage.py
M Tools/clinic/libclinic/converters.py
M Tools/clinic/libclinic/dsl_parser.py
M Tools/clinic/libclinic/function.py
M Tools/clinic/libclinic/parse_args.py

diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c
index 6d3c42ddbd03fed..528e88369735303 100644
--- a/Lib/test/clinic.test.c
+++ b/Lib/test/clinic.test.c
@@ -5303,6 +5303,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;
@@ -5310,7 +5316,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
@@ -5335,6 +5374,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;
@@ -5342,7 +5387,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 6f6e2cc34652642..91b7626fdec846a 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -767,6 +767,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)
+
 
 class ParseFileUnitTest(TestCase):
     def expect_parsing_failure(
@@ -2528,7 +2624,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"""
@@ -2539,7 +2635,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):
@@ -2582,9 +2678,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 e62f29a6b387ad3..d20ecf308e419cf 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -1404,10 +1404,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) {
@@ -1447,10 +1443,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;
@@ -1614,10 +1606,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;
@@ -2478,10 +2466,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 d152653d121b33c..12fef3ea23fef96 100644
--- a/Modules/_ctypes/_ctypes.c
+++ b/Modules/_ctypes/_ctypes.c
@@ -1502,10 +1502,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;
@@ -1561,12 +1557,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 +3661,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 +3699,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 +3763,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);
@@ -5414,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 92dfb8f83b7da6d..62414cf1816df22 100644
--- a/Modules/_ctypes/clinic/_ctypes.c.h
+++ b/Modules/_ctypes/clinic/_ctypes.c.h
@@ -425,6 +425,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();
@@ -1000,4 +1006,4 @@ Simple_from_outparm(PyObject *self, PyTypeObject *cls, 
PyObject *const *args, Py
     }
     return Simple_from_outparm_impl(self, cls);
 }
-/*[clinic end generated code: output=9fb75bf7e9a17df2 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=349a8678c2782fc9 input=a9049054013a1b77]*/
diff --git a/Modules/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h
index 8d59bda5f74b386..99a3eb9f2607076 100644
--- a/Modules/_io/clinic/textio.c.h
+++ b/Modules/_io/clinic/textio.c.h
@@ -1325,10 +1325,16 @@ _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();
 
     return return_value;
 }
-/*[clinic end generated code: output=8c571c9dba87d2b1 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=cec74a7476964ae4 input=a9049054013a1b77]*/
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index 74775e035e47a44..4815aa26ab59862 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -3326,10 +3326,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 f07a09df5ac7ae0..d92026814920391 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();
@@ -2233,4 +2257,4 @@ _asyncio_future_discard_from_awaited_by(PyObject *module, 
PyObject *const *args,
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=32996fb47c48245b input=a9049054013a1b77]*/
+/*[clinic end generated code: output=6a0d02bd421248aa input=a9049054013a1b77]*/
diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h
index 0f45b2f52c50902..d105500d176b155 100644
--- a/Modules/clinic/_ssl.c.h
+++ b/Modules/clinic/_ssl.c.h
@@ -316,6 +316,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();
@@ -439,6 +445,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();
@@ -735,6 +747,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();
@@ -941,6 +959,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();
@@ -991,6 +1015,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();
@@ -1042,6 +1072,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();
@@ -1093,6 +1129,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();
@@ -1150,6 +1192,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();
@@ -1232,6 +1280,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();
@@ -1282,6 +1336,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();
@@ -1332,6 +1392,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();
@@ -1866,6 +1932,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();
@@ -2906,4 +2978,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=6b5d14b14e152522 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=8a0e36a31fccd58c 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 833e283c17603c8..932143996408739 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -352,12 +352,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) {
@@ -391,13 +392,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");
@@ -436,13 +438,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");
@@ -479,13 +482,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 b4597a937074b8f..a73d9b485a76f22 100644
--- a/Objects/frameobject.c
+++ b/Objects/frameobject.c
@@ -1641,10 +1641,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,
@@ -1856,12 +1852,13 @@ frame_trace_get_impl(PyFrameObject *self)
 /*[clinic input]
 @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=d96a18bda085707f]*/
+/*[clinic end generated code: output=d6fe08335cf76ae4 input=b652d64fdd1189f2]*/
 {
     if (value == Py_None) {
         value = NULL;
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index b59a493e6234cb5..412c7ec4d0a66e6 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -941,12 +941,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,
@@ -995,12 +996,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;
@@ -1040,12 +1042,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 48be6f5add352d6..208689b1a76d409 100644
--- a/Python/traceback.c
+++ b/Python/traceback.c
@@ -177,12 +177,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 3ee06307441fac4..2e381477674df6f 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:
@@ -463,12 +464,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")
@@ -483,7 +484,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 64fc1e95007516e..2756aa7370eb406 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)
@@ -1129,7 +1129,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 6ead9bf20228334..ee8d09dee75470b 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 (
@@ -439,21 +439,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:
@@ -574,7 +584,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.
@@ -587,8 +597,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"
@@ -608,7 +618,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()
 
@@ -714,6 +724,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 *":
@@ -796,9 +822,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)
@@ -1302,7 +1327,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:
@@ -1473,7 +1498,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 e80e2f5f13f648e..8f9be33372d54a1 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 0e15d2f163b8161..b4a359df8670f34 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:
@@ -315,7 +331,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 {return_value};"
@@ -374,9 +390,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"
@@ -861,7 +880,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

_______________________________________________
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