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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/acdecb345d6a5803ba11faf25b3d2d2cf088ed4b
commit: acdecb345d6a5803ba11faf25b3d2d2cf088ed4b
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-18T13:09:10Z
summary:

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

(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/_io/clinic/textio.c.h
M Modules/_io/textio.c
M Modules/_sqlite/clinic/cursor.c.h
M Modules/clinic/_ssl.c.h
M Objects/funcobject.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 7e0761f12497cff..4aac53998aa73f4 100644
--- a/Lib/test/clinic.test.c
+++ b/Lib/test/clinic.test.c
@@ -5002,6 +5002,12 @@ Test_property_set(TestObj *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(self, value);
 
     return return_value;
@@ -5009,7 +5015,40 @@ Test_property_set(TestObj *self, PyObject *value, void *Py_UNUSED(context))
 
 static int
 Test_property_set_impl(TestObj *self, PyObject *value)
-/*[clinic end generated code: output=e4342fe9bb1d7817 input=3bc3f46a23c83a88]*/
+/*[clinic end generated code: output=4bfe2c5a55b47aa1 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(TestObj *self, PyObject *value, void *Py_UNUSED(context))
+{
+    int return_value;
+
+    return_value = Test_settable_and_deletable_set_impl(self, value);
+
+    return return_value;
+}
+
+static int
+Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value)
+/*[clinic end generated code: output=fd94dde2a5e99d0b input=f5647f3511b9daea]*/
 
 /*[clinic input]
 @setter
@@ -5034,6 +5073,12 @@ Test_setter_first_with_docstr_set(TestObj *self, PyObject *value, void *Py_UNUSE
 {
     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(self, value);
 
     return return_value;
@@ -5041,7 +5086,7 @@ Test_setter_first_with_docstr_set(TestObj *self, PyObject *value, void *Py_UNUSE
 
 static int
 Test_setter_first_with_docstr_set_impl(TestObj *self, PyObject *value)
-/*[clinic end generated code: output=e4d76b558a4061db input=31a045ce11bbe961]*/
+/*[clinic end generated code: output=176716b785b29167 input=31a045ce11bbe961]*/
 
 /*[clinic input]
 @getter
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index feb1e2b25d59c35..2c5812d97b0dee7 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -761,6 +761,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(
@@ -2345,7 +2441,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"""
@@ -2356,7 +2452,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):
@@ -2399,9 +2495,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..69ca0b32e238404
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst
@@ -0,0 +1,4 @@
+Fix crashes when deleting an attribute whose setter is generated by Argument
+Clinic and is not prepared for deletion, among them 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/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h
index f35538ee75fa48a..94cb4eaff5b20e4 100644
--- a/Modules/_io/clinic/textio.c.h
+++ b/Modules/_io/clinic/textio.c.h
@@ -1280,10 +1280,16 @@ _io_TextIOWrapper__CHUNK_SIZE_set(textio *self, PyObject *value, void *Py_UNUSED
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
 
     return return_value;
 }
-/*[clinic end generated code: output=2d73d5adb0ce09a8 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=a63aa1779c11cfff input=a9049054013a1b77]*/
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index 3b3d4bed7bfca69..ed69f995369cce8 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -3307,10 +3307,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 56b849dc0b63595..40ca66141f7b19c 100644
--- a/Modules/_sqlite/clinic/cursor.c.h
+++ b/Modules/_sqlite/clinic/cursor.c.h
@@ -351,8 +351,14 @@ _sqlite3_Cursor_arraysize_set(pysqlite_Cursor *self, PyObject *value, void *Py_U
 {
     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(self, value);
 
     return return_value;
 }
-/*[clinic end generated code: output=332c298249ae57b0 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=b08f9d3f49ccee9f input=a9049054013a1b77]*/
diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h
index 2cb60b7dca8cc6c..1926b4742abffaf 100644
--- a/Modules/clinic/_ssl.c.h
+++ b/Modules/clinic/_ssl.c.h
@@ -316,6 +316,12 @@ _ssl__SSLSocket_context_set(PySSLSocket *self, PyObject *value, void *Py_UNUSED(
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -439,6 +445,12 @@ _ssl__SSLSocket_owner_set(PySSLSocket *self, PyObject *value, void *Py_UNUSED(co
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -732,6 +744,12 @@ _ssl__SSLSocket_session_set(PySSLSocket *self, PyObject *value, void *Py_UNUSED(
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -938,6 +956,12 @@ _ssl__SSLContext_verify_mode_set(PySSLContext *self, PyObject *value, void *Py_U
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -988,6 +1012,12 @@ _ssl__SSLContext_verify_flags_set(PySSLContext *self, PyObject *value, void *Py_
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1039,6 +1069,12 @@ _ssl__SSLContext_minimum_version_set(PySSLContext *self, PyObject *value, void *
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1090,6 +1126,12 @@ _ssl__SSLContext_maximum_version_set(PySSLContext *self, PyObject *value, void *
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1147,6 +1189,12 @@ _ssl__SSLContext_num_tickets_set(PySSLContext *self, PyObject *value, void *Py_U
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1229,6 +1277,12 @@ _ssl__SSLContext_options_set(PySSLContext *self, PyObject *value, void *Py_UNUSE
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1279,6 +1333,12 @@ _ssl__SSLContext__host_flags_set(PySSLContext *self, PyObject *value, void *Py_U
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1329,6 +1389,12 @@ _ssl__SSLContext_check_hostname_set(PySSLContext *self, PyObject *value, void *P
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -1851,6 +1917,12 @@ _ssl__SSLContext_sni_callback_set(PySSLContext *self, PyObject *value, void *Py_
 {
     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(self, value);
     Py_END_CRITICAL_SECTION();
@@ -2873,4 +2945,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=d6407f7dbbc5d5b7 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=a08f8b28363ea35a input=a9049054013a1b77]*/
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index a39b5464f4d4acf..ee5ce785db2a29a 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -809,12 +809,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;
@@ -853,12 +854,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/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py
index 165ab40621fc6ce..3e9e791b2b48ab6 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:
@@ -466,12 +467,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")
@@ -486,7 +487,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 238237d7b2ec831..473eb32718eafb2 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)
@@ -1110,7 +1110,7 @@ def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> st
 def correct_name_for_self(
         f: Function
 ) -> 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 27c042155eba7cd..85a0f6c576242d0 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 (
@@ -425,21 +425,31 @@ def at_critical_section(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:
@@ -560,7 +570,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.
@@ -573,8 +583,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"
@@ -594,7 +604,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()
 
@@ -699,6 +709,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 *":
@@ -781,9 +807,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)
@@ -1339,7 +1364,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:
@@ -1510,7 +1535,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 93901263e44c045..f314680e0d669af 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)
@@ -160,7 +167,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 1e6ebdd68367b59..9a1d2e52efb65c3 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)
@@ -186,6 +187,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:
@@ -303,7 +319,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};"
@@ -361,9 +377,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"
@@ -866,7 +885,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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.