gh-112205: Support `@setter` annotation from AC (gh-112922)

---------

Co-authored-by: Erlend E. Aasland <erlend.aasland@protonmail.com>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Donghee Na 2023-12-13 14:00:34 +00:00 committed by GitHub
parent 9263173280
commit 498a096a51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 262 additions and 64 deletions

View 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

View 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 = """

View 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}
};

View 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]*/

View 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]*/

View 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]*/

View 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}
};

View 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}
};

View File

@ -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 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['getter_name'] = f.c_basename.upper() + "_GETTERDEF"
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,6 +1993,10 @@ class CLanguage(Language):
converter.set_template_dict(template_dict)
f.return_converter.render(f, data)
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))
@ -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)