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]