]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-112205: Support `@setter` annotation from AC (gh-112922)
authorDonghee Na <donghee.na@python.org>
Wed, 13 Dec 2023 14:00:34 +0000 (14:00 +0000)
committerGitHub <noreply@github.com>
Wed, 13 Dec 2023 14:00:34 +0000 (14:00 +0000)
---------

Co-authored-by: Erlend E. Aasland <erlend.aasland@protonmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Lib/test/clinic.test.c
Lib/test/test_clinic.py
Modules/_io/bufferedio.c
Modules/_io/clinic/bufferedio.c.h
Modules/_io/clinic/stringio.c.h
Modules/_io/clinic/textio.c.h
Modules/_io/stringio.c
Modules/_io/textio.c
Tools/clinic/clinic.py

index ee4a4228fd28beaecd56ff19b93c69eff025d367..a6a21664bb82a1a0a8a42010e8f96bb0f00efbe4 100644 (file)
@@ -4956,8 +4956,12 @@ Test_meth_coexist_impl(TestObj *self)
 Test.property
 [clinic start generated code]*/
 
-#define TEST_PROPERTY_GETTERDEF    \
-    {"property", (getter)Test_property_get, NULL, NULL},
+#if defined(TEST_PROPERTY_GETSETDEF)
+#  undef TEST_PROPERTY_GETSETDEF
+#  define TEST_PROPERTY_GETSETDEF {"property", (getter)Test_property_get, (setter)Test_property_set, NULL},
+#else
+#  define TEST_PROPERTY_GETSETDEF {"property", (getter)Test_property_get, NULL, NULL},
+#endif
 
 static PyObject *
 Test_property_get_impl(TestObj *self);
@@ -4970,8 +4974,32 @@ Test_property_get(TestObj *self, void *Py_UNUSED(context))
 
 static PyObject *
 Test_property_get_impl(TestObj *self)
-/*[clinic end generated code: output=892b6fb351ff85fd input=2d92b3449fbc7d2b]*/
+/*[clinic end generated code: output=af8140b692e0e2f1 input=2d92b3449fbc7d2b]*/
+
+/*[clinic input]
+@setter
+Test.property
+[clinic start generated code]*/
+
+#if defined(TEST_PROPERTY_GETSETDEF)
+#  undef TEST_PROPERTY_GETSETDEF
+#  define TEST_PROPERTY_GETSETDEF {"property", (getter)Test_property_get, (setter)Test_property_set, NULL},
+#else
+#  define TEST_PROPERTY_GETSETDEF {"property", NULL, (setter)Test_property_set, NULL},
+#endif
+
+static int
+Test_property_set_impl(TestObj *self, PyObject *value);
+
+static int
+Test_property_set(TestObj *self, PyObject *value, void *Py_UNUSED(context))
+{
+    return Test_property_set_impl(self, value);
+}
 
+static int
+Test_property_set_impl(TestObj *self, PyObject *value)
+/*[clinic end generated code: output=f3eba6487d7550e2 input=3bc3f46a23c83a88]*/
 
 /*[clinic input]
 output push
index f53e9481083106a0be3d3bdc5e39413db12cd06c..d3dbde88dd82a9e82eb3517726d382e1e41380e1 100644 (file)
@@ -2197,6 +2197,58 @@ class ClinicParserTest(TestCase):
                 expected_error = err_template.format(invalid_kind)
                 self.expect_failure(block, expected_error, lineno=3)
 
+    def test_invalid_getset(self):
+        annotations = ["@getter", "@setter"]
+        for annotation in annotations:
+            with self.subTest(annotation=annotation):
+                block = f"""
+                    module foo
+                    class Foo "" ""
+                    {annotation}
+                    Foo.property -> int
+                """
+                expected_error = f"{annotation} method cannot define a return type"
+                self.expect_failure(block, expected_error, lineno=3)
+
+                block = f"""
+                   module foo
+                   class Foo "" ""
+                   {annotation}
+                   Foo.property
+                       obj: int
+                       /
+                """
+                expected_error = f"{annotation} method cannot define parameters"
+                self.expect_failure(block, expected_error)
+
+    def test_duplicate_getset(self):
+        annotations = ["@getter", "@setter"]
+        for annotation in annotations:
+            with self.subTest(annotation=annotation):
+                block = f"""
+                    module foo
+                    class Foo "" ""
+                    {annotation}
+                    {annotation}
+                    Foo.property -> int
+                """
+                expected_error = f"Cannot apply {annotation} twice to the same function!"
+                self.expect_failure(block, expected_error, lineno=3)
+
+    def test_getter_and_setter_disallowed_on_same_function(self):
+        dup_annotations = [("@getter", "@setter"), ("@setter", "@getter")]
+        for dup in dup_annotations:
+            with self.subTest(dup=dup):
+                block = f"""
+                    module foo
+                    class Foo "" ""
+                    {dup[0]}
+                    {dup[1]}
+                    Foo.property -> int
+                """
+                expected_error = "Cannot apply both @getter and @setter to the same function!"
+                self.expect_failure(block, expected_error, lineno=3)
+
     def test_duplicate_coexist(self):
         err = "Called @coexist twice"
         block = """
index 679626863c385c108c669d3b64b4f191d093ad3d..f02207ace9f3d2626f40c2bf27aa4108960295b1 100644 (file)
@@ -2526,9 +2526,9 @@ static PyMemberDef bufferedreader_members[] = {
 };
 
 static PyGetSetDef bufferedreader_getset[] = {
-    _IO__BUFFERED_CLOSED_GETTERDEF
-    _IO__BUFFERED_NAME_GETTERDEF
-    _IO__BUFFERED_MODE_GETTERDEF
+    _IO__BUFFERED_CLOSED_GETSETDEF
+    _IO__BUFFERED_NAME_GETSETDEF
+    _IO__BUFFERED_MODE_GETSETDEF
     {NULL}
 };
 
@@ -2586,9 +2586,9 @@ static PyMemberDef bufferedwriter_members[] = {
 };
 
 static PyGetSetDef bufferedwriter_getset[] = {
-    _IO__BUFFERED_CLOSED_GETTERDEF
-    _IO__BUFFERED_NAME_GETTERDEF
-    _IO__BUFFERED_MODE_GETTERDEF
+    _IO__BUFFERED_CLOSED_GETSETDEF
+    _IO__BUFFERED_NAME_GETSETDEF
+    _IO__BUFFERED_MODE_GETSETDEF
     {NULL}
 };
 
@@ -2704,9 +2704,9 @@ static PyMemberDef bufferedrandom_members[] = {
 };
 
 static PyGetSetDef bufferedrandom_getset[] = {
-    _IO__BUFFERED_CLOSED_GETTERDEF
-    _IO__BUFFERED_NAME_GETTERDEF
-    _IO__BUFFERED_MODE_GETTERDEF
+    _IO__BUFFERED_CLOSED_GETSETDEF
+    _IO__BUFFERED_NAME_GETSETDEF
+    _IO__BUFFERED_MODE_GETSETDEF
     {NULL}
 };
 
index 69d28ad00c2ad50a62876fdc24238b80d1200c5e..ec46d5409a3d824157266efcd111956b869e59ca 100644 (file)
@@ -327,8 +327,12 @@ _io__Buffered_simple_flush(buffered *self, PyObject *Py_UNUSED(ignored))
     return return_value;
 }
 
-#define _IO__BUFFERED_CLOSED_GETTERDEF    \
-    {"closed", (getter)_io__Buffered_closed_get, NULL, NULL},
+#if defined(_IO__BUFFERED_CLOSED_GETSETDEF)
+#  undef _IO__BUFFERED_CLOSED_GETSETDEF
+#  define _IO__BUFFERED_CLOSED_GETSETDEF {"closed", (getter)_io__Buffered_closed_get, (setter)_io__Buffered_closed_set, NULL},
+#else
+#  define _IO__BUFFERED_CLOSED_GETSETDEF {"closed", (getter)_io__Buffered_closed_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io__Buffered_closed_get_impl(buffered *self);
@@ -460,8 +464,12 @@ _io__Buffered_writable(buffered *self, PyObject *Py_UNUSED(ignored))
     return return_value;
 }
 
-#define _IO__BUFFERED_NAME_GETTERDEF    \
-    {"name", (getter)_io__Buffered_name_get, NULL, NULL},
+#if defined(_IO__BUFFERED_NAME_GETSETDEF)
+#  undef _IO__BUFFERED_NAME_GETSETDEF
+#  define _IO__BUFFERED_NAME_GETSETDEF {"name", (getter)_io__Buffered_name_get, (setter)_io__Buffered_name_set, NULL},
+#else
+#  define _IO__BUFFERED_NAME_GETSETDEF {"name", (getter)_io__Buffered_name_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io__Buffered_name_get_impl(buffered *self);
@@ -478,8 +486,12 @@ _io__Buffered_name_get(buffered *self, void *Py_UNUSED(context))
     return return_value;
 }
 
-#define _IO__BUFFERED_MODE_GETTERDEF    \
-    {"mode", (getter)_io__Buffered_mode_get, NULL, NULL},
+#if defined(_IO__BUFFERED_MODE_GETSETDEF)
+#  undef _IO__BUFFERED_MODE_GETSETDEF
+#  define _IO__BUFFERED_MODE_GETSETDEF {"mode", (getter)_io__Buffered_mode_get, (setter)_io__Buffered_mode_set, NULL},
+#else
+#  define _IO__BUFFERED_MODE_GETSETDEF {"mode", (getter)_io__Buffered_mode_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io__Buffered_mode_get_impl(buffered *self);
@@ -1218,4 +1230,4 @@ skip_optional_pos:
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=f21ed03255032b43 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=0999c33f666dc692 input=a9049054013a1b77]*/
index ed505ae67589a89c830ea104c9d1a7b9ee817259..fc2962d1c9c9a704706ae4eb9b32add3cfdf57f1 100644 (file)
@@ -475,8 +475,12 @@ _io_StringIO___setstate__(stringio *self, PyObject *state)
     return return_value;
 }
 
-#define _IO_STRINGIO_CLOSED_GETTERDEF    \
-    {"closed", (getter)_io_StringIO_closed_get, NULL, NULL},
+#if defined(_IO_STRINGIO_CLOSED_GETSETDEF)
+#  undef _IO_STRINGIO_CLOSED_GETSETDEF
+#  define _IO_STRINGIO_CLOSED_GETSETDEF {"closed", (getter)_io_StringIO_closed_get, (setter)_io_StringIO_closed_set, NULL},
+#else
+#  define _IO_STRINGIO_CLOSED_GETSETDEF {"closed", (getter)_io_StringIO_closed_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io_StringIO_closed_get_impl(stringio *self);
@@ -493,8 +497,12 @@ _io_StringIO_closed_get(stringio *self, void *Py_UNUSED(context))
     return return_value;
 }
 
-#define _IO_STRINGIO_LINE_BUFFERING_GETTERDEF    \
-    {"line_buffering", (getter)_io_StringIO_line_buffering_get, NULL, NULL},
+#if defined(_IO_STRINGIO_LINE_BUFFERING_GETSETDEF)
+#  undef _IO_STRINGIO_LINE_BUFFERING_GETSETDEF
+#  define _IO_STRINGIO_LINE_BUFFERING_GETSETDEF {"line_buffering", (getter)_io_StringIO_line_buffering_get, (setter)_io_StringIO_line_buffering_set, NULL},
+#else
+#  define _IO_STRINGIO_LINE_BUFFERING_GETSETDEF {"line_buffering", (getter)_io_StringIO_line_buffering_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io_StringIO_line_buffering_get_impl(stringio *self);
@@ -511,8 +519,12 @@ _io_StringIO_line_buffering_get(stringio *self, void *Py_UNUSED(context))
     return return_value;
 }
 
-#define _IO_STRINGIO_NEWLINES_GETTERDEF    \
-    {"newlines", (getter)_io_StringIO_newlines_get, NULL, NULL},
+#if defined(_IO_STRINGIO_NEWLINES_GETSETDEF)
+#  undef _IO_STRINGIO_NEWLINES_GETSETDEF
+#  define _IO_STRINGIO_NEWLINES_GETSETDEF {"newlines", (getter)_io_StringIO_newlines_get, (setter)_io_StringIO_newlines_set, NULL},
+#else
+#  define _IO_STRINGIO_NEWLINES_GETSETDEF {"newlines", (getter)_io_StringIO_newlines_get, NULL, NULL},
+#endif
 
 static PyObject *
 _io_StringIO_newlines_get_impl(stringio *self);
@@ -528,4 +540,4 @@ _io_StringIO_newlines_get(stringio *self, void *Py_UNUSED(context))
 
     return return_value;
 }
-/*[clinic end generated code: output=3a92e8b6c322f61b input=a9049054013a1b77]*/
+/*[clinic end generated code: output=27726751d98ab617 input=a9049054013a1b77]*/
index 675e0ed2eab75e18ceeeb349d963f8f38603d5b5..a492f340c74c0ddfb6af666a842543ad146e5cb1 100644 (file)
@@ -1047,4 +1047,48 @@ _io_TextIOWrapper_close(textio *self, PyObject *Py_UNUSED(ignored))
 
     return return_value;
 }
-/*[clinic end generated code: output=8781a91be6d99e2c input=a9049054013a1b77]*/
+
+#if defined(_IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF)
+#  undef _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF
+#  define _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF {"_CHUNK_SIZE", (getter)_io_TextIOWrapper__CHUNK_SIZE_get, (setter)_io_TextIOWrapper__CHUNK_SIZE_set, NULL},
+#else
+#  define _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF {"_CHUNK_SIZE", (getter)_io_TextIOWrapper__CHUNK_SIZE_get, NULL, NULL},
+#endif
+
+static PyObject *
+_io_TextIOWrapper__CHUNK_SIZE_get_impl(textio *self);
+
+static PyObject *
+_io_TextIOWrapper__CHUNK_SIZE_get(textio *self, void *Py_UNUSED(context))
+{
+    PyObject *return_value = NULL;
+
+    Py_BEGIN_CRITICAL_SECTION(self);
+    return_value = _io_TextIOWrapper__CHUNK_SIZE_get_impl(self);
+    Py_END_CRITICAL_SECTION();
+
+    return return_value;
+}
+
+#if defined(_IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF)
+#  undef _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF
+#  define _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF {"_CHUNK_SIZE", (getter)_io_TextIOWrapper__CHUNK_SIZE_get, (setter)_io_TextIOWrapper__CHUNK_SIZE_set, NULL},
+#else
+#  define _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF {"_CHUNK_SIZE", NULL, (setter)_io_TextIOWrapper__CHUNK_SIZE_set, NULL},
+#endif
+
+static int
+_io_TextIOWrapper__CHUNK_SIZE_set_impl(textio *self, PyObject *value);
+
+static int
+_io_TextIOWrapper__CHUNK_SIZE_set(textio *self, PyObject *value, void *Py_UNUSED(context))
+{
+    int return_value;
+
+    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=b312f2d2e2221580 input=a9049054013a1b77]*/
index 74dcee2373030696f31e8da4a87054bfae64e540..06bc2679e8e2276ff25de487bc9ec75908afcfca 100644 (file)
@@ -1037,15 +1037,15 @@ static struct PyMethodDef stringio_methods[] = {
 };
 
 static PyGetSetDef stringio_getset[] = {
-    _IO_STRINGIO_CLOSED_GETTERDEF
-    _IO_STRINGIO_NEWLINES_GETTERDEF
+    _IO_STRINGIO_CLOSED_GETSETDEF
+    _IO_STRINGIO_NEWLINES_GETSETDEF
     /*  (following comments straight off of the original Python wrapper:)
         XXX Cruft to support the TextIOWrapper API. This would only
         be meaningful if StringIO supported the buffer attribute.
         Hopefully, a better solution, than adding these pseudo-attributes,
         will be found.
     */
-    _IO_STRINGIO_LINE_BUFFERING_GETTERDEF
+    _IO_STRINGIO_LINE_BUFFERING_GETSETDEF
     {NULL}
 };
 
index 545f467b7f0257be26e5369b45e063b8be8a216e..c76d92cdd38b9a751e745b41eb633366eea7cbbc 100644 (file)
@@ -3238,33 +3238,37 @@ textiowrapper_errors_get(textio *self, void *context)
     return result;
 }
 
+/*[clinic input]
+@critical_section
+@getter
+_io.TextIOWrapper._CHUNK_SIZE
+[clinic start generated code]*/
+
 static PyObject *
-textiowrapper_chunk_size_get_impl(textio *self, void *context)
+_io_TextIOWrapper__CHUNK_SIZE_get_impl(textio *self)
+/*[clinic end generated code: output=039925cd2df375bc input=e9715b0e06ff0fa6]*/
 {
     CHECK_ATTACHED(self);
     return PyLong_FromSsize_t(self->chunk_size);
 }
 
-static PyObject *
-textiowrapper_chunk_size_get(textio *self, void *context)
-{
-    PyObject *result = NULL;
-    Py_BEGIN_CRITICAL_SECTION(self);
-    result = textiowrapper_chunk_size_get_impl(self, context);
-    Py_END_CRITICAL_SECTION();
-    return result;
-}
+/*[clinic input]
+@critical_section
+@setter
+_io.TextIOWrapper._CHUNK_SIZE
+[clinic start generated code]*/
 
 static int
-textiowrapper_chunk_size_set_impl(textio *self, PyObject *arg, void *context)
+_io_TextIOWrapper__CHUNK_SIZE_set_impl(textio *self, PyObject *value)
+/*[clinic end generated code: output=edb86d2db660a5ab input=32fc99861db02a0a]*/
 {
     Py_ssize_t n;
     CHECK_ATTACHED_INT(self);
-    if (arg == NULL) {
+    if (value == NULL) {
         PyErr_SetString(PyExc_AttributeError, "cannot delete attribute");
         return -1;
     }
-    n = PyNumber_AsSsize_t(arg, PyExc_ValueError);
+    n = PyNumber_AsSsize_t(value, PyExc_ValueError);
     if (n == -1 && PyErr_Occurred())
         return -1;
     if (n <= 0) {
@@ -3276,16 +3280,6 @@ textiowrapper_chunk_size_set_impl(textio *self, PyObject *arg, void *context)
     return 0;
 }
 
-static int
-textiowrapper_chunk_size_set(textio *self, PyObject *arg, void *context)
-{
-    int result = 0;
-    Py_BEGIN_CRITICAL_SECTION(self);
-    result = textiowrapper_chunk_size_set_impl(self, arg, context);
-    Py_END_CRITICAL_SECTION();
-    return result;
-}
-
 static PyMethodDef incrementalnewlinedecoder_methods[] = {
     _IO_INCREMENTALNEWLINEDECODER_DECODE_METHODDEF
     _IO_INCREMENTALNEWLINEDECODER_GETSTATE_METHODDEF
@@ -3361,8 +3355,7 @@ static PyGetSetDef textiowrapper_getset[] = {
 */
     {"newlines", (getter)textiowrapper_newlines_get, NULL, NULL},
     {"errors", (getter)textiowrapper_errors_get, NULL, NULL},
-    {"_CHUNK_SIZE", (getter)textiowrapper_chunk_size_get,
-                    (setter)textiowrapper_chunk_size_set, NULL},
+    _IO_TEXTIOWRAPPER__CHUNK_SIZE_GETSETDEF
     {NULL}
 };
 
index 816ce0e6efed6106377642291bf127d70a40d275..5ec088765f3e01ad1e8917736b01756bb2563272 100755 (executable)
@@ -850,6 +850,10 @@ class CLanguage(Language):
         static PyObject *
         {c_basename}({self_type}{self_name}, void *Py_UNUSED(context))
     """)
+    PARSER_PROTOTYPE_SETTER: Final[str] = normalize_snippet("""
+        static int
+        {c_basename}({self_type}{self_name}, PyObject *value, void *Py_UNUSED(context))
+    """)
     METH_O_PROTOTYPE: Final[str] = normalize_snippet("""
         static PyObject *
         {c_basename}({impl_parameters})
@@ -870,8 +874,20 @@ class CLanguage(Language):
             {{"{name}", {methoddef_cast}{c_basename}{methoddef_cast_end}, {methoddef_flags}, {c_basename}__doc__}},
     """)
     GETTERDEF_PROTOTYPE_DEFINE: Final[str] = normalize_snippet(r"""
-        #define {getter_name}    \
-            {{"{name}", (getter){c_basename}, NULL, NULL}},
+        #if defined({getset_name}_GETSETDEF)
+        #  undef {getset_name}_GETSETDEF
+        #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, (setter){getset_basename}_set, NULL}},
+        #else
+        #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, NULL, NULL}},
+        #endif
+    """)
+    SETTERDEF_PROTOTYPE_DEFINE: Final[str] = normalize_snippet(r"""
+        #if defined({getset_name}_GETSETDEF)
+        #  undef {getset_name}_GETSETDEF
+        #  define {getset_name}_GETSETDEF {{"{name}", (getter){getset_basename}_get, (setter){getset_basename}_set, NULL}},
+        #else
+        #  define {getset_name}_GETSETDEF {{"{name}", NULL, (setter){getset_basename}_set, NULL}},
+        #endif
     """)
     METHODDEF_PROTOTYPE_IFNDEF: Final[str] = normalize_snippet("""
         #ifndef {methoddef_name}
@@ -1172,6 +1188,10 @@ class CLanguage(Language):
         elif f.kind is GETTER:
             methoddef_define = self.GETTERDEF_PROTOTYPE_DEFINE
             docstring_prototype = docstring_definition = ''
+        elif f.kind is SETTER:
+            return_value_declaration = "int {return_value};"
+            methoddef_define = self.SETTERDEF_PROTOTYPE_DEFINE
+            docstring_prototype = docstring_prototype = docstring_definition = ''
         else:
             docstring_prototype = self.DOCSTRING_PROTOTYPE_VAR
             docstring_definition = self.DOCSTRING_PROTOTYPE_STRVAR
@@ -1226,12 +1246,19 @@ class CLanguage(Language):
             limited_capi = False
 
         parsearg: str | None
+        if f.kind in {GETTER, SETTER} and parameters:
+            fail(f"@{f.kind.name.lower()} method cannot define parameters")
+
         if not parameters:
             parser_code: list[str] | None
             if f.kind is GETTER:
                 flags = "" # This should end up unused
                 parser_prototype = self.PARSER_PROTOTYPE_GETTER
                 parser_code = []
+            elif f.kind is SETTER:
+                flags = ""
+                parser_prototype = self.PARSER_PROTOTYPE_SETTER
+                parser_code = []
             elif not requires_defining_class:
                 # no parameters, METH_NOARGS
                 flags = "METH_NOARGS"
@@ -1944,9 +1971,16 @@ class CLanguage(Language):
         full_name = f.full_name
         template_dict = {'full_name': full_name}
         template_dict['name'] = f.displayname
-        if f.kind is GETTER:
-            template_dict['getter_name'] = f.c_basename.upper() + "_GETTERDEF"
-            template_dict['c_basename'] = f.c_basename + "_get"
+        if f.kind in {GETTER, SETTER}:
+            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:
+                template_dict['c_basename'] = f.c_basename + "_set"
+                # Implicitly add the setter value parameter.
+                data.impl_parameters.append("PyObject *value")
+                data.impl_arguments.append("value")
         else:
             template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF"
             template_dict['c_basename'] = f.c_basename
@@ -1959,7 +1993,11 @@ class CLanguage(Language):
             converter.set_template_dict(template_dict)
 
         f.return_converter.render(f, data)
-        template_dict['impl_return_type'] = f.return_converter.type
+        if f.kind is SETTER:
+            # All setters return an int.
+            template_dict['impl_return_type'] = 'int'
+        else:
+            template_dict['impl_return_type'] = f.return_converter.type
 
         template_dict['declarations'] = format_escape("\n".join(data.declarations))
         template_dict['initializers'] = "\n\n".join(data.initializers)
@@ -2954,6 +2992,7 @@ class FunctionKind(enum.Enum):
     METHOD_INIT     = enum.auto()
     METHOD_NEW      = enum.auto()
     GETTER          = enum.auto()
+    SETTER          = enum.auto()
 
     @functools.cached_property
     def new_or_init(self) -> bool:
@@ -2970,6 +3009,7 @@ CLASS_METHOD: Final = FunctionKind.CLASS_METHOD
 METHOD_INIT: Final = FunctionKind.METHOD_INIT
 METHOD_NEW: Final = FunctionKind.METHOD_NEW
 GETTER: Final = FunctionKind.GETTER
+SETTER: Final = FunctionKind.SETTER
 
 ParamDict = dict[str, "Parameter"]
 ReturnConverterType = Callable[..., "CReturnConverter"]
@@ -3056,7 +3096,7 @@ class Function:
             case FunctionKind.STATIC_METHOD:
                 flags.append('METH_STATIC')
             case _ as kind:
-                acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER}
+                acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER}
                 assert kind in acceptable_kinds, f"unknown kind: {kind!r}"
         if self.coexist:
             flags.append('METH_COEXIST')
@@ -4702,7 +4742,7 @@ class Py_buffer_converter(CConverter):
 def correct_name_for_self(
         f: Function
 ) -> tuple[str, str]:
-    if f.kind in {CALLABLE, METHOD_INIT, GETTER}:
+    if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}:
         if f.cls:
             return "PyObject *", "self"
         return "PyObject *", "module"
@@ -5335,7 +5375,22 @@ class DSLParser:
         self.critical_section = True
 
     def at_getter(self) -> None:
-        self.kind = GETTER
+        match self.kind:
+            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
+
+    def at_setter(self) -> None:
+        match self.kind:
+            case FunctionKind.SETTER:
+                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
 
     def at_staticmethod(self) -> None:
         if self.kind is not CALLABLE:
@@ -5536,6 +5591,8 @@ class DSLParser:
 
         return_converter = None
         if returns:
+            if self.kind in {GETTER, SETTER}:
+                fail(f"@{self.kind.name.lower()} method cannot define a return type")
             ast_input = f"def x() -> {returns}: pass"
             try:
                 module_node = ast.parse(ast_input)