From bd36b7c4bafd4a234748762cb4872a134be976be Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 19 Aug 2026 21:11:51 +0300 Subject: [PATCH] gh-113318: Support @deleter as a separate function in Argument Clinic Used alone, the @deleter directive defines a function which deletes the attribute. It can be combined with a @setter defined in a separate block: the setter slot of the entry is then filled with a function which dispatches to one of them depending on whether the value is NULL. function.__annotations__ is now implemented in this way. --- Lib/test/clinic.test.c | 100 ++++++++++- Lib/test/test_clinic.py | 168 +++++++++++++----- ...-08-19-11-05-00.gh-issue-113318.Hb3vQm.rst | 5 + Objects/clinic/funcobject.c.h | 43 ++++- Objects/funcobject.c | 23 ++- Tools/clinic/libclinic/clanguage.py | 28 ++- Tools/clinic/libclinic/dsl_parser.py | 36 ++-- Tools/clinic/libclinic/function.py | 18 +- Tools/clinic/libclinic/parse_args.py | 60 ++++++- 9 files changed, 402 insertions(+), 79 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-08-19-11-05-00.gh-issue-113318.Hb3vQm.rst diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index fa71df7f4f9d0ed..cf25e957e3dde6b 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5380,6 +5380,34 @@ static PyObject * Test_meth_coexist_impl(TestObj *self) /*[clinic end generated code: output=7edf4e95b29f06fa input=2a1d75b5e6fec6dd]*/ +/*[clinic input] +@deleter +Test.deletable +[clinic start generated code]*/ + +static int +Test_deletable_del_impl(TestObj *self); + +static int +Test_deletable_del(PyObject *self, PyObject *arg, void *Py_UNUSED(context)) +{ + int return_value = -1; + + if (arg != NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'deletable' of '%.100s' objects is not writable", + Py_TYPE(self)->tp_name); + return -1; + } + return_value = Test_deletable_del_impl((TestObj *)self); + + return return_value; +} + +static int +Test_deletable_del_impl(TestObj *self) +/*[clinic end generated code: output=01336ded38587734 input=4fcdd5171ed08c49]*/ + /*[clinic input] @setter @deleter @@ -5407,6 +5435,64 @@ static int Test_settable_set_impl(TestObj *self, PyObject *value) /*[clinic end generated code: output=46832806d93e5391 input=c5e1780ba116abdc]*/ +/*[clinic input] +@setter +Test.settable_deletable +[clinic start generated code]*/ + +static int +Test_settable_deletable_set_impl(TestObj *self, PyObject *value); + +static int +Test_settable_deletable_set(PyObject *self, PyObject *arg, void *Py_UNUSED(context)) +{ + int return_value = -1; + PyObject *value; + + if (arg == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'settable_deletable' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } + value = arg; + return_value = Test_settable_deletable_set_impl((TestObj *)self, value); + + return return_value; +} + +static int +Test_settable_deletable_set_impl(TestObj *self, PyObject *value) +/*[clinic end generated code: output=acf1cc489e57a1d2 input=a7c956963a35028a]*/ + +/*[clinic input] +@deleter +Test.settable_deletable +[clinic start generated code]*/ + +static int +Test_settable_deletable_del_impl(TestObj *self); + +static int +Test_settable_deletable_del(PyObject *self, PyObject *arg, void *Py_UNUSED(context)) +{ + int return_value = -1; + + if (arg != NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'settable_deletable' of '%.100s' objects is not writable", + Py_TYPE(self)->tp_name); + return -1; + } + return_value = Test_settable_deletable_del_impl((TestObj *)self); + + return return_value; +} + +static int +Test_settable_deletable_del_impl(TestObj *self) +/*[clinic end generated code: output=8b93e59f2a8f0f69 input=731adc7d73cb3cf9]*/ + /*[clinic input] @getter Test.int_property -> int @@ -5574,15 +5660,27 @@ Test_setter_first_with_docstr_get_impl(TestObj *self) /*[clinic input] dump buffer [clinic start generated code]*/ +#define TEST_DELETABLE_GETSETDEF {"deletable", (getter)NULL, (setter)Test_deletable_del, NULL}, + #define TEST_SETTABLE_GETSETDEF {"settable", (getter)NULL, (setter)Test_settable_set, NULL}, +static int +Test_settable_deletable_set_or_del(PyObject *self, PyObject *value, void *context) +{ + if (value == NULL) { + return ((setter)Test_settable_deletable_del)(self, value, context); + } + return ((setter)Test_settable_deletable_set)(self, value, context); +} +#define TEST_SETTABLE_DELETABLE_GETSETDEF {"settable_deletable", (getter)NULL, (setter)Test_settable_deletable_set_or_del, NULL}, + #define TEST_INT_PROPERTY_GETSETDEF {"int_property", (getter)Test_int_property_get, (setter)Test_int_property_set, NULL}, #define TEST_PROPERTY_GETSETDEF {"property", (getter)Test_property_get, (setter)Test_property_set, NULL}, #define TEST_SETTER_FIRST_WITH_DOCSTR_GETSETDEF {"setter_first_with_docstr", (getter)Test_setter_first_with_docstr_get, (setter)Test_setter_first_with_docstr_set, Test_setter_first_with_docstr__doc__}, -/*[clinic end generated code: output=011497a4f5a2e835 input=524ce2e021e4eba6]*/ +/*[clinic end generated code: output=76a58036c91356d4 input=524ce2e021e4eba6]*/ /*[clinic input] output push diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index f5334f70768dc45..25b16a1e2e65f4e 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -868,8 +868,7 @@ class Foo "FooObject *" "&Foo_Type" "#define FOO_PROPERTY_GETTER Foo_property_get\n", generated) self.assertIn("#define FOO_PROPERTY_SETTER Foo_property_set\n" - "#if defined(FOO_PROPERTY_GETTER) " - "|| defined(FOO_PROPERTY_SETTER)", + "#if defined(FOO_PROPERTY_SETTER)", generated) self.assertIn('# define FOO_PROPERTY_GETSETDEF {"property", ' '(getter)FOO_PROPERTY_GETTER, ' @@ -962,8 +961,8 @@ def test_deleter(self): class Foo "FooObject *" "&Foo_Type" [clinic start generated code]*/ /*[clinic input] - @setter @deleter + @setter Foo.property value: object = NULL [clinic start generated code]*/ @@ -971,12 +970,67 @@ class Foo "FooObject *" "&Foo_Type" generated = self.clinic.parse(dedent(block)) self.assertNotIn("if (arg == NULL) {", generated) + def test_standalone_deleter(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @deleter + Foo.property + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + # The deleter fills the setter slot and rejects assignment. + self.assertIn('#define FOO_PROPERTY_GETSETDEF {"property", ' + '(getter)NULL, (setter)Foo_property_del, NULL},', + generated) + self.assertIn("if (arg != NULL) {", generated) + self.assertIn("Foo_property_del_impl((FooObject *)self)", generated) + + def test_setter_and_standalone_deleter(self): + block = """ + /*[clinic input] + output everything block + output methoddef_ifndef buffer + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property + + value: object + [clinic start generated code]*/ + /*[clinic input] + @deleter + Foo.property + [clinic start generated code]*/ + /*[clinic input] + dump buffer + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + # The setter slot is filled with a function which dispatches + # to the setter or to the deleter. + self.assertIn(" return ((setter)Foo_property_del)" + "(self, value, context);\n" + " }\n" + " return ((setter)Foo_property_set)" + "(self, value, context);", + generated) + self.assertIn('#define FOO_PROPERTY_GETSETDEF {"property", ' + '(getter)NULL, (setter)Foo_property_set_or_del, NULL},', + generated) + def test_getset_duplicate(self): # Only a setter defines the new value. for annotation, parameter, err in ( ("@getter", "", "Cannot apply @getter to 'Foo.property' twice"), ("@setter", "value: object", "The setter of 'Foo.property' is already defined"), + ("@deleter", "", + "The deleter of 'Foo.property' is already defined"), ): with self.subTest(annotation=annotation): self.clinic = _make_clinic(filename="test.c") @@ -997,6 +1051,26 @@ class Foo "FooObject *" "&Foo_Type" """ self.expect_failure(block, err, lineno=11) + def test_deleter_and_standalone_deleter(self): + block = """ + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + @deleter + Foo.property + + value: object = NULL + [clinic start generated code]*/ + /*[clinic input] + @deleter + Foo.property + [clinic start generated code]*/ + """ + err = "The deleter of 'Foo.property' is already defined" + self.expect_failure(block, err, lineno=13) + def test_getset_different_c_basename(self): block = """ /*[clinic input] @@ -3004,25 +3078,30 @@ class Foo "" "" self.expect_failure(block, expected_error, lineno=1) def test_invalid_getset(self): - block = """ - module foo - class Foo "" "" - @setter - Foo.property -> int - """ - expected_error = "@setter methods cannot define a return type" - self.expect_failure(block, expected_error, lineno=3) + for annotation in ("@setter", "@deleter"): + with self.subTest(annotation=annotation): + block = f""" + module foo + class Foo "" "" + {annotation} + Foo.property -> int + """ + expected_error = ("@setter and @deleter methods cannot " + "define a return type") + self.expect_failure(block, expected_error, lineno=3) - block = """ - module foo - class Foo "" "" - @getter - Foo.property - obj: int - / - """ - expected_error = "@getter methods cannot define parameters" - self.expect_failure(block, expected_error) + for annotation in ("@getter", "@deleter"): + with self.subTest(annotation=annotation): + block = f""" + module foo + class Foo "" "" + {annotation} + Foo.property + obj: int + / + """ + expected_error = f"{annotation} methods cannot define parameters" + self.expect_failure(block, expected_error) block = """ module foo @@ -3050,8 +3129,8 @@ class Foo "" "" block = """ module m class Foo "" "" - @setter @deleter + @setter Foo.property value: object """ @@ -3174,46 +3253,43 @@ class Foo "" "" f"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_getter_and_deleter_disallowed_on_same_function(self): + dup_annotations = [("@getter", "@deleter"), ("@deleter", "@getter")] + for dup in dup_annotations: + with self.subTest(dup=dup): + block = f""" + module foo + class Foo "" "" + {dup[0]} + {dup[1]} + Foo.property + """ + expected_error = (f"Can't set {dup[1]}, function is not " + f"a normal callable") + self.expect_failure(block, expected_error, lineno=3) - def test_deleter_twice(self): + def test_duplicate_deleter(self): block = """ - module foo + module m class Foo "" "" - @setter @deleter @deleter + @setter Foo.property """ expected_error = "Cannot apply @deleter twice to the same function!" - self.expect_failure(block, expected_error, lineno=4) + self.expect_failure(block, expected_error, lineno=3) def test_getset_no_class(self): - for annotation in "@getter", "@setter": + for annotation in "@getter", "@setter", "@deleter": with self.subTest(annotation=annotation): block = f""" module m {annotation} m.func """ - expected_error = "@getter and @setter must be methods" + expected_error = ("@getter, @setter and @deleter " + "must be methods") self.expect_failure(block, expected_error, lineno=2) def test_duplicate_coexist(self): diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-19-11-05-00.gh-issue-113318.Hb3vQm.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-19-11-05-00.gh-issue-113318.Hb3vQm.rst new file mode 100644 index 000000000000000..c07220092b2feec --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-19-11-05-00.gh-issue-113318.Hb3vQm.rst @@ -0,0 +1,5 @@ +Used alone, the ``@deleter`` directive in Argument Clinic now defines a +separate function which deletes the attribute. +It can be combined with a ``@setter`` defined in a separate block: the setter +slot of the :c:type:`PyGetSetDef` entry is then filled with a function which +dispatches to one of them. diff --git a/Objects/clinic/funcobject.c.h b/Objects/clinic/funcobject.c.h index fa581e553aca8ae..01195f9bbfbb6c5 100644 --- a/Objects/clinic/funcobject.c.h +++ b/Objects/clinic/funcobject.c.h @@ -75,11 +75,15 @@ static int function___annotations___set(PyObject *self, PyObject *arg, void *Py_UNUSED(context)) { int return_value = -1; - PyObject *value = NULL; + PyObject *value; - if (arg != NULL) { - value = arg; + if (arg == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '__annotations__' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; } + value = arg; Py_BEGIN_CRITICAL_SECTION(self); return_value = function___annotations___set_impl((PyFunctionObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -87,6 +91,27 @@ function___annotations___set(PyObject *self, PyObject *arg, void *Py_UNUSED(cont return return_value; } +static int +function___annotations___del_impl(PyFunctionObject *self); + +static int +function___annotations___del(PyObject *self, PyObject *arg, void *Py_UNUSED(context)) +{ + int return_value = -1; + + if (arg != NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '__annotations__' of '%.100s' objects is not writable", + Py_TYPE(self)->tp_name); + return -1; + } + Py_BEGIN_CRITICAL_SECTION(self); + return_value = function___annotations___del_impl((PyFunctionObject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(function___type_params____doc__, "Get the declared type parameters for a function."); @@ -245,8 +270,16 @@ func_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) } #define FUNCTION___ANNOTATE___GETSETDEF {"__annotate__", (getter)function___annotate___get, (setter)function___annotate___set, function___annotate____doc__}, -#define FUNCTION___ANNOTATIONS___GETSETDEF {"__annotations__", (getter)function___annotations___get, (setter)function___annotations___set, function___annotations____doc__}, +static int +function___annotations___set_or_del(PyObject *self, PyObject *value, void *context) +{ + if (value == NULL) { + return ((setter)function___annotations___del)(self, value, context); + } + return ((setter)function___annotations___set)(self, value, context); +} +#define FUNCTION___ANNOTATIONS___GETSETDEF {"__annotations__", (getter)function___annotations___get, (setter)function___annotations___set_or_del, function___annotations____doc__}, #define FUNCTION___TYPE_PARAMS___GETSETDEF {"__type_params__", (getter)function___type_params___get, (setter)function___type_params___set, function___type_params____doc__}, -/*[clinic end generated code: output=b722bea4e9d8b8be input=a9049054013a1b77]*/ +/*[clinic end generated code: output=916d95458bdae4f6 input=a9049054013a1b77]*/ diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 4d3da234f87e494..7ec5cf1c5fb6db6 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -975,19 +975,17 @@ 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=71f6a58c00ac6745]*/ +/*[clinic end generated code: output=a61795d4a95eede4 input=5302641f686f0463]*/ { + /* Can only set func_annotations to None (which clears it, as deletion + * does) or to a dict. */ if (value == Py_None) value = NULL; - /* Legal to del f.func_annotations. - * Can only set func_annotations to NULL (through C api) - * or a dict. */ if (value != NULL && !PyDict_Check(value)) { PyErr_SetString(PyExc_TypeError, "__annotations__ must be set to a dict object"); @@ -998,6 +996,21 @@ function___annotations___set_impl(PyFunctionObject *self, PyObject *value) return 0; } +/*[clinic input] +@critical_section +@deleter +function.__annotations__ +[clinic start generated code]*/ + +static int +function___annotations___del_impl(PyFunctionObject *self) +/*[clinic end generated code: output=e68e5ec4145b0768 input=4014003acb737ff7]*/ +{ + Py_CLEAR(self->func_annotations); + Py_CLEAR(self->func_annotate); + return 0; +} + /*[clinic input] @critical_section @getter diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index 1f71c6cda81b81b..6dd905cc21276b6 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -17,7 +17,7 @@ from libclinic.converters import self_converter from libclinic.parse_args import ( ParseArgsCodeGen, - GETSETDEF_PROTOTYPE_COMBINE, GETSETDEF_PROTOTYPE_DEFINE) + GETSETDEF_PROTOTYPE_COMBINE, GETSETDEF_PROTOTYPE_DEFINE, SETDEL_DISPATCHER) if TYPE_CHECKING: from libclinic.app import Clinic @@ -109,18 +109,33 @@ def render_property(self, prop: Property) -> str: template_dict = { 'name': prop.name, 'getset_name': getset_name, + 'setdel_basename': prop.setdel_basename, } parts: list[str] = [] if prop.is_plain: getter = prop.getter[0] if prop.getter else None setter = prop.setter[0] if prop.setter else None + deleter = prop.deleter[0] if prop.deleter else None template_dict['getter'] = (getter.accessor_basename if getter else 'NULL') - template_dict['setter'] = (setter.accessor_basename if setter - else 'NULL') template_dict['docstr'] = (f'{getter.c_basename}__doc__' if getter and getter.docstring else 'NULL') + if setter is None: + # The deleter alone fills the setter slot of the entry. + setter, deleter = deleter, None + elif setter is deleter: + # The setter is called with NULL to delete the attribute. + deleter = None + template_dict['setter'] = (setter.accessor_basename if setter + else 'NULL') + if deleter is not None: + # Setting and deleting are implemented by different + # functions, so an intermediate function dispatches to one of + # them. + template_dict['deleter'] = deleter.accessor_basename + parts.append(SETDEL_DISPATCHER.format_map(template_dict) + '\n') + template_dict['setter'] = prop.setdel_basename parts.append(GETSETDEF_PROTOTYPE_DEFINE.format_map(template_dict) + '\n') else: @@ -128,10 +143,15 @@ def render_property(self, prop: Property) -> str: # only known to the preprocessor. They announce themselves, so # only the unconditional ones are announced here. for suffix, funcs in (('GETTER', prop.getter), - ('SETTER', prop.setter)): + ('SETTER', prop.setter), + ('DELETER', prop.deleter)): for func in funcs: if func.condition: continue + if suffix == 'DELETER' and func in prop.setter: + # The setter is called with NULL to delete the + # attribute, so it does not need a deleter. + continue parts.append(f"#define {getset_name}_{suffix} " f"{func.accessor_basename}\n") if suffix == 'GETTER' and func.docstring: diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index a798fac4f3fd096..0c98beff656e1bc 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -18,7 +18,7 @@ Module, Class, Property, Function, Parameter, FunctionKind, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, - GETTER, SETTER, SETTER_AND_DELETER, + GETTER, SETTER, DELETER, SETTER_AND_DELETER, ACCESSORS, SETTERS) from libclinic.converter import ( converters, legacy_converters) @@ -468,6 +468,9 @@ def at_setter(self) -> None: match self.kind: case FunctionKind.CALLABLE: self.kind = FunctionKind.SETTER + case FunctionKind.DELETER: + # The setter is called with NULL to delete the attribute. + self.kind = FunctionKind.SETTER_AND_DELETER case FunctionKind.SETTER | FunctionKind.SETTER_AND_DELETER: fail("Cannot apply @setter twice to the same function!") case _: @@ -475,13 +478,16 @@ def at_setter(self) -> None: def at_deleter(self) -> None: match self.kind: + case FunctionKind.CALLABLE: + # @deleter alone defines a separate deletion function. + self.kind = FunctionKind.DELETER case FunctionKind.SETTER: # The setter is called with NULL to delete the attribute. self.kind = FunctionKind.SETTER_AND_DELETER - case FunctionKind.SETTER_AND_DELETER: + case FunctionKind.DELETER | FunctionKind.SETTER_AND_DELETER: fail("Cannot apply @deleter twice to the same function!") case _: - fail("Can't set @deleter, @setter is not applied") + fail("Can't set @deleter, function is not a normal callable") def at_staticmethod(self) -> None: if self.kind is not CALLABLE: @@ -626,7 +632,7 @@ def normalize_function_kind(self, fullname: str) -> None: if name == '__new__' and (self.kind is not CLASS_METHOD or not cls): fail("'__new__' must be a class method!") if self.kind in ACCESSORS and not cls: - fail("@getter and @setter must be methods") + fail("@getter, @setter and @deleter must be methods") # Normalise self.kind. if name == '__new__': @@ -650,7 +656,7 @@ def resolve_return_converter( ) -> CReturnConverter: if forced_converter: if self.kind in SETTERS: - fail("@setter methods cannot define a return type") + fail("@setter and @deleter 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" @@ -817,14 +823,23 @@ def add_accessor(self, func: Function) -> None: func.cls.properties[func.name] = prop self.clinic.properties.append(prop) func.property = prop - slot = prop.getter if func.kind is GETTER else prop.setter + if func.kind is GETTER: + self.fill_accessor_slot(prop.getter, func, 'getter') + else: + if func.kind is not DELETER: + self.fill_accessor_slot(prop.setter, func, 'setter') + if func.kind is not SETTER: + self.fill_accessor_slot(prop.deleter, func, 'deleter') + + def fill_accessor_slot(self, slot: list[Function], func: Function, + name: str) -> None: # Several implementations of the same accessor can share the slot if # each of them is compiled under its own preprocessor condition. if slot and (not func.condition or any(not other.condition for other in slot)): - if func.kind is GETTER: + if name == 'getter': fail(f"Cannot apply @getter to {func.full_name!r} twice") - fail(f"The setter of {func.full_name!r} is already defined") + fail(f"The {name} of {func.full_name!r} is already defined") slot.append(func) # Now entering the parameters section. The rules, formally stated: @@ -891,8 +906,9 @@ 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 is GETTER: - fail("@getter methods cannot define parameters") + if self.function.kind is GETTER or self.function.kind is DELETER: + kind = self.function.kind.name.lower() + fail(f"@{kind} methods cannot define parameters") self.parameter_continuation = '' return self.next(self.state_parameter, line) diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index e5d7469bb732aa9..0173b74f1521277 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -59,7 +59,8 @@ class Property: """An attribute implemented by accessors, rendered into a PyGetSetDef entry. A slot can contain several implementations if they are guarded by - preprocessor conditions. + preprocessor conditions, and the same function if it is both the setter + and the deleter. """ name: str full_name: str @@ -68,6 +69,7 @@ class Property: def __post_init__(self) -> None: self.getter: list[Function] = [] self.setter: list[Function] = [] + self.deleter: list[Function] = [] self.rendered = False def __repr__(self) -> str: @@ -77,13 +79,19 @@ def __repr__(self) -> str: def is_plain(self) -> bool: """Can the entry be composed without the help of the preprocessor?""" return all(len(funcs) <= 1 and not (funcs and funcs[0].condition) - for funcs in (self.getter, self.setter)) + for funcs in (self.getter, self.setter, self.deleter)) @property def getset_name(self) -> str: """The prefix of the names of the macros of the entry.""" return self.full_name.replace('.', '_').upper() + @property + def setdel_basename(self) -> str: + """The name of the function which dispatches to the setter or the + deleter.""" + return self.full_name.replace('.', '_') + "_set_or_del" + class FunctionKind(enum.Enum): CALLABLE = enum.auto() @@ -93,6 +101,7 @@ class FunctionKind(enum.Enum): METHOD_NEW = enum.auto() GETTER = enum.auto() SETTER = enum.auto() + DELETER = enum.auto() SETTER_AND_DELETER = enum.auto() @functools.cached_property @@ -110,10 +119,11 @@ def __repr__(self) -> str: METHOD_NEW: Final = FunctionKind.METHOD_NEW GETTER: Final = FunctionKind.GETTER SETTER: Final = FunctionKind.SETTER +DELETER: Final = FunctionKind.DELETER 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}) +SETTERS: Final = frozenset({SETTER, DELETER, SETTER_AND_DELETER}) # The kinds which implement an entry of PyGetSetDef. ACCESSORS: Final = SETTERS | {GETTER} @@ -172,6 +182,8 @@ def accessor_basename(self) -> str: assert self.kind in ACCESSORS if self.kind is GETTER: return self.c_basename + "_get" + if self.kind is DELETER: + return self.c_basename + "_del" return self.c_basename + "_set" @functools.cached_property diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 4caab253fe32ccf..30d167f222d91da 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -6,7 +6,7 @@ from libclinic.function import ( Function, Parameter, ParamTuple, count_required, group_to_variable_name, permute_optional_groups, - GETTER, SETTER, SETTER_AND_DELETER, METHOD_INIT, + GETTER, SETTER, DELETER, SETTER_AND_DELETER, METHOD_INIT, ACCESSORS, SETTERS) from libclinic.converter import CConverter from libclinic.converters import ( @@ -191,14 +191,38 @@ def declare_parser( return -1; }} """, indent=4) +DELETER_PREAMBLE: Final[str] = libclinic.normalize_snippet(""" + if (arg != NULL) {{ + PyErr_Format(PyExc_AttributeError, + "attribute '{name}' of '%.100s' objects is not writable", + Py_TYPE({self_name})->tp_name); + return -1; + }} +""", indent=4) SETTERDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(""" #define {getset_name}_SETTER {c_basename} """) +DELETERDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(""" + #define {getset_name}_DELETER {c_basename} +""") METHODDEF_PROTOTYPE_IFNDEF: Final[str] = libclinic.normalize_snippet(""" #ifndef {methoddef_name} #define {methoddef_name} #endif /* !defined({methoddef_name}) */ """) +# The setter slot of a PyGetSetDef entry is used both for setting and for +# deleting the attribute. If they are implemented by separate functions, an +# intermediate function dispatches to one of them. +SETDEL_DISPATCHER: Final[str] = libclinic.normalize_snippet(""" + static int + {setdel_basename}(PyObject *self, PyObject *value, void *context) + {{ + if (value == NULL) {{ + return ((setter){deleter})(self, value, context); + }} + return ((setter){setter})(self, value, context); + }} +""") # The entry of an attribute whose accessors are all compiled unconditionally. GETSETDEF_PROTOTYPE_DEFINE: Final[str] = libclinic.normalize_snippet(""" #define {getset_name}_GETSETDEF {{"{name}", (getter){getter}, (setter){setter}, {docstr}}}, @@ -206,7 +230,25 @@ def declare_parser( # Composes the PyGetSetDef entry of an attribute. It must be rendered after # all accessors of that attribute, so it is written to the same destination as # the "ifndef" of a method, which is emptied at the end of the file. +# +# The setter slot of the entry is used both for setting and for deleting the +# attribute. If they are implemented by separate functions, an intermediate +# function dispatches to one of them. GETSETDEF_PROTOTYPE_COMBINE: Final[str] = libclinic.normalize_snippet(""" + #if defined({getset_name}_SETTER) && defined({getset_name}_DELETER) + static int + {setdel_basename}(PyObject *self, PyObject *value, void *context) + {{ + if (value == NULL) {{ + return ((setter){getset_name}_DELETER)(self, value, context); + }} + return ((setter){getset_name}_SETTER)(self, value, context); + }} + # undef {getset_name}_SETTER + # define {getset_name}_SETTER {setdel_basename} + #elif defined({getset_name}_DELETER) + # define {getset_name}_SETTER {getset_name}_DELETER + #endif #if defined({getset_name}_GETTER) || defined({getset_name}_SETTER) # if !defined({getset_name}_GETTER) # define {getset_name}_GETTER NULL @@ -421,8 +463,12 @@ def select_prototypes(self) -> None: line_number=self.func.line_number) # The conversion of the value can fail before it is set. self.return_value_declaration = "int {parser_retval} = -1;" - self.methoddef_define = (SETTERDEF_PROTOTYPE_DEFINE - if self.func.condition else '') + if self.func.kind is DELETER: + self.methoddef_define = (DELETERDEF_PROTOTYPE_DEFINE + if self.func.condition else '') + else: + self.methoddef_define = (SETTERDEF_PROTOTYPE_DEFINE + if self.func.condition else '') else: self.docstring_prototype = DOCSTRING_PROTOTYPE_VAR self.docstring_definition = DOCSTRING_PROTOTYPE_STRVAR @@ -483,7 +529,7 @@ def render_setter_value(self) -> str: ]) def parse_accessor(self) -> None: - """Generate the code of a getter or a setter.""" + """Generate the code of a getter, a setter or a deleter.""" parser_code: list[str] = [] if self.func.kind is GETTER: self.parser_prototype = PARSER_PROTOTYPE_GETTER @@ -494,7 +540,11 @@ def parse_accessor(self) -> None: if self.func.kind is SETTER: # The setter which is not the deleter rejects the deletion. parser_code.append(SETTER_PREAMBLE) - parser_code.append(self.render_setter_value()) + elif self.func.kind is DELETER: + # The deleter is called without a value. + parser_code.append(DELETER_PREAMBLE) + if self.func.kind is not DELETER: + parser_code.append(self.render_setter_value()) self.finish_parser_body(parser_code) def parse_no_args(self) -> None: