Skip to content

Commit bd36b7c

Browse files
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.
1 parent 6b5d414 commit bd36b7c

9 files changed

Lines changed: 402 additions & 79 deletions

File tree

‎Lib/test/clinic.test.c‎

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5380,6 +5380,34 @@ static PyObject *
53805380
Test_meth_coexist_impl(TestObj *self)
53815381
/*[clinic end generated code: output=7edf4e95b29f06fa input=2a1d75b5e6fec6dd]*/
53825382

5383+
/*[clinic input]
5384+
@deleter
5385+
Test.deletable
5386+
[clinic start generated code]*/
5387+
5388+
static int
5389+
Test_deletable_del_impl(TestObj *self);
5390+
5391+
static int
5392+
Test_deletable_del(PyObject *self, PyObject *arg, void *Py_UNUSED(context))
5393+
{
5394+
int return_value = -1;
5395+
5396+
if (arg != NULL) {
5397+
PyErr_Format(PyExc_AttributeError,
5398+
"attribute 'deletable' of '%.100s' objects is not writable",
5399+
Py_TYPE(self)->tp_name);
5400+
return -1;
5401+
}
5402+
return_value = Test_deletable_del_impl((TestObj *)self);
5403+
5404+
return return_value;
5405+
}
5406+
5407+
static int
5408+
Test_deletable_del_impl(TestObj *self)
5409+
/*[clinic end generated code: output=01336ded38587734 input=4fcdd5171ed08c49]*/
5410+
53835411
/*[clinic input]
53845412
@setter
53855413
@deleter
@@ -5407,6 +5435,64 @@ static int
54075435
Test_settable_set_impl(TestObj *self, PyObject *value)
54085436
/*[clinic end generated code: output=46832806d93e5391 input=c5e1780ba116abdc]*/
54095437

5438+
/*[clinic input]
5439+
@setter
5440+
Test.settable_deletable
5441+
[clinic start generated code]*/
5442+
5443+
static int
5444+
Test_settable_deletable_set_impl(TestObj *self, PyObject *value);
5445+
5446+
static int
5447+
Test_settable_deletable_set(PyObject *self, PyObject *arg, void *Py_UNUSED(context))
5448+
{
5449+
int return_value = -1;
5450+
PyObject *value;
5451+
5452+
if (arg == NULL) {
5453+
PyErr_Format(PyExc_AttributeError,
5454+
"attribute 'settable_deletable' of '%.100s' objects cannot be deleted",
5455+
Py_TYPE(self)->tp_name);
5456+
return -1;
5457+
}
5458+
value = arg;
5459+
return_value = Test_settable_deletable_set_impl((TestObj *)self, value);
5460+
5461+
return return_value;
5462+
}
5463+
5464+
static int
5465+
Test_settable_deletable_set_impl(TestObj *self, PyObject *value)
5466+
/*[clinic end generated code: output=acf1cc489e57a1d2 input=a7c956963a35028a]*/
5467+
5468+
/*[clinic input]
5469+
@deleter
5470+
Test.settable_deletable
5471+
[clinic start generated code]*/
5472+
5473+
static int
5474+
Test_settable_deletable_del_impl(TestObj *self);
5475+
5476+
static int
5477+
Test_settable_deletable_del(PyObject *self, PyObject *arg, void *Py_UNUSED(context))
5478+
{
5479+
int return_value = -1;
5480+
5481+
if (arg != NULL) {
5482+
PyErr_Format(PyExc_AttributeError,
5483+
"attribute 'settable_deletable' of '%.100s' objects is not writable",
5484+
Py_TYPE(self)->tp_name);
5485+
return -1;
5486+
}
5487+
return_value = Test_settable_deletable_del_impl((TestObj *)self);
5488+
5489+
return return_value;
5490+
}
5491+
5492+
static int
5493+
Test_settable_deletable_del_impl(TestObj *self)
5494+
/*[clinic end generated code: output=8b93e59f2a8f0f69 input=731adc7d73cb3cf9]*/
5495+
54105496
/*[clinic input]
54115497
@getter
54125498
Test.int_property -> int
@@ -5574,15 +5660,27 @@ Test_setter_first_with_docstr_get_impl(TestObj *self)
55745660
/*[clinic input]
55755661
dump buffer
55765662
[clinic start generated code]*/
5663+
#define TEST_DELETABLE_GETSETDEF {"deletable", (getter)NULL, (setter)Test_deletable_del, NULL},
5664+
55775665
#define TEST_SETTABLE_GETSETDEF {"settable", (getter)NULL, (setter)Test_settable_set, NULL},
55785666

5667+
static int
5668+
Test_settable_deletable_set_or_del(PyObject *self, PyObject *value, void *context)
5669+
{
5670+
if (value == NULL) {
5671+
return ((setter)Test_settable_deletable_del)(self, value, context);
5672+
}
5673+
return ((setter)Test_settable_deletable_set)(self, value, context);
5674+
}
5675+
#define TEST_SETTABLE_DELETABLE_GETSETDEF {"settable_deletable", (getter)NULL, (setter)Test_settable_deletable_set_or_del, NULL},
5676+
55795677
#define TEST_INT_PROPERTY_GETSETDEF {"int_property", (getter)Test_int_property_get, (setter)Test_int_property_set, NULL},
55805678

55815679
#define TEST_PROPERTY_GETSETDEF {"property", (getter)Test_property_get, (setter)Test_property_set, NULL},
55825680

55835681
#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__},
55845682

5585-
/*[clinic end generated code: output=011497a4f5a2e835 input=524ce2e021e4eba6]*/
5683+
/*[clinic end generated code: output=76a58036c91356d4 input=524ce2e021e4eba6]*/
55865684

55875685
/*[clinic input]
55885686
output push

‎Lib/test/test_clinic.py‎

Lines changed: 122 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -868,8 +868,7 @@ class Foo "FooObject *" "&Foo_Type"
868868
"#define FOO_PROPERTY_GETTER Foo_property_get\n",
869869
generated)
870870
self.assertIn("#define FOO_PROPERTY_SETTER Foo_property_set\n"
871-
"#if defined(FOO_PROPERTY_GETTER) "
872-
"|| defined(FOO_PROPERTY_SETTER)",
871+
"#if defined(FOO_PROPERTY_SETTER)",
873872
generated)
874873
self.assertIn('# define FOO_PROPERTY_GETSETDEF {"property", '
875874
'(getter)FOO_PROPERTY_GETTER, '
@@ -962,21 +961,76 @@ def test_deleter(self):
962961
class Foo "FooObject *" "&Foo_Type"
963962
[clinic start generated code]*/
964963
/*[clinic input]
965-
@setter
966964
@deleter
965+
@setter
967966
Foo.property
968967
value: object = NULL
969968
[clinic start generated code]*/
970969
"""
971970
generated = self.clinic.parse(dedent(block))
972971
self.assertNotIn("if (arg == NULL) {", generated)
973972

973+
def test_standalone_deleter(self):
974+
block = """
975+
/*[clinic input]
976+
output everything block
977+
class Foo "FooObject *" "&Foo_Type"
978+
[clinic start generated code]*/
979+
/*[clinic input]
980+
@deleter
981+
Foo.property
982+
[clinic start generated code]*/
983+
"""
984+
generated = self.clinic.parse(dedent(block))
985+
# The deleter fills the setter slot and rejects assignment.
986+
self.assertIn('#define FOO_PROPERTY_GETSETDEF {"property", '
987+
'(getter)NULL, (setter)Foo_property_del, NULL},',
988+
generated)
989+
self.assertIn("if (arg != NULL) {", generated)
990+
self.assertIn("Foo_property_del_impl((FooObject *)self)", generated)
991+
992+
def test_setter_and_standalone_deleter(self):
993+
block = """
994+
/*[clinic input]
995+
output everything block
996+
output methoddef_ifndef buffer
997+
class Foo "FooObject *" "&Foo_Type"
998+
[clinic start generated code]*/
999+
/*[clinic input]
1000+
@setter
1001+
Foo.property
1002+
1003+
value: object
1004+
[clinic start generated code]*/
1005+
/*[clinic input]
1006+
@deleter
1007+
Foo.property
1008+
[clinic start generated code]*/
1009+
/*[clinic input]
1010+
dump buffer
1011+
[clinic start generated code]*/
1012+
"""
1013+
generated = self.clinic.parse(dedent(block))
1014+
# The setter slot is filled with a function which dispatches
1015+
# to the setter or to the deleter.
1016+
self.assertIn(" return ((setter)Foo_property_del)"
1017+
"(self, value, context);\n"
1018+
" }\n"
1019+
" return ((setter)Foo_property_set)"
1020+
"(self, value, context);",
1021+
generated)
1022+
self.assertIn('#define FOO_PROPERTY_GETSETDEF {"property", '
1023+
'(getter)NULL, (setter)Foo_property_set_or_del, NULL},',
1024+
generated)
1025+
9741026
def test_getset_duplicate(self):
9751027
# Only a setter defines the new value.
9761028
for annotation, parameter, err in (
9771029
("@getter", "", "Cannot apply @getter to 'Foo.property' twice"),
9781030
("@setter", "value: object",
9791031
"The setter of 'Foo.property' is already defined"),
1032+
("@deleter", "",
1033+
"The deleter of 'Foo.property' is already defined"),
9801034
):
9811035
with self.subTest(annotation=annotation):
9821036
self.clinic = _make_clinic(filename="test.c")
@@ -997,6 +1051,26 @@ class Foo "FooObject *" "&Foo_Type"
9971051
"""
9981052
self.expect_failure(block, err, lineno=11)
9991053

1054+
def test_deleter_and_standalone_deleter(self):
1055+
block = """
1056+
/*[clinic input]
1057+
class Foo "FooObject *" "&Foo_Type"
1058+
[clinic start generated code]*/
1059+
/*[clinic input]
1060+
@setter
1061+
@deleter
1062+
Foo.property
1063+
1064+
value: object = NULL
1065+
[clinic start generated code]*/
1066+
/*[clinic input]
1067+
@deleter
1068+
Foo.property
1069+
[clinic start generated code]*/
1070+
"""
1071+
err = "The deleter of 'Foo.property' is already defined"
1072+
self.expect_failure(block, err, lineno=13)
1073+
10001074
def test_getset_different_c_basename(self):
10011075
block = """
10021076
/*[clinic input]
@@ -3004,25 +3078,30 @@ class Foo "" ""
30043078
self.expect_failure(block, expected_error, lineno=1)
30053079

30063080
def test_invalid_getset(self):
3007-
block = """
3008-
module foo
3009-
class Foo "" ""
3010-
@setter
3011-
Foo.property -> int
3012-
"""
3013-
expected_error = "@setter methods cannot define a return type"
3014-
self.expect_failure(block, expected_error, lineno=3)
3081+
for annotation in ("@setter", "@deleter"):
3082+
with self.subTest(annotation=annotation):
3083+
block = f"""
3084+
module foo
3085+
class Foo "" ""
3086+
{annotation}
3087+
Foo.property -> int
3088+
"""
3089+
expected_error = ("@setter and @deleter methods cannot "
3090+
"define a return type")
3091+
self.expect_failure(block, expected_error, lineno=3)
30153092

3016-
block = """
3017-
module foo
3018-
class Foo "" ""
3019-
@getter
3020-
Foo.property
3021-
obj: int
3022-
/
3023-
"""
3024-
expected_error = "@getter methods cannot define parameters"
3025-
self.expect_failure(block, expected_error)
3093+
for annotation in ("@getter", "@deleter"):
3094+
with self.subTest(annotation=annotation):
3095+
block = f"""
3096+
module foo
3097+
class Foo "" ""
3098+
{annotation}
3099+
Foo.property
3100+
obj: int
3101+
/
3102+
"""
3103+
expected_error = f"{annotation} methods cannot define parameters"
3104+
self.expect_failure(block, expected_error)
30263105

30273106
block = """
30283107
module foo
@@ -3050,8 +3129,8 @@ class Foo "" ""
30503129
block = """
30513130
module m
30523131
class Foo "" ""
3053-
@setter
30543132
@deleter
3133+
@setter
30553134
Foo.property
30563135
value: object
30573136
"""
@@ -3174,46 +3253,43 @@ class Foo "" ""
31743253
f"a normal callable")
31753254
self.expect_failure(block, expected_error, lineno=3)
31763255

3177-
def test_deleter_without_setter(self):
3178-
block = """
3179-
module foo
3180-
class Foo "" ""
3181-
@deleter
3182-
Foo.property
3183-
"""
3184-
expected_error = "Can't set @deleter, @setter is not applied"
3185-
self.expect_failure(block, expected_error, lineno=2)
3186-
3187-
block = """
3188-
module foo
3189-
class Foo "" ""
3190-
@deleter
3191-
@setter
3192-
Foo.property
3193-
"""
3194-
self.expect_failure(block, expected_error, lineno=2)
3256+
def test_getter_and_deleter_disallowed_on_same_function(self):
3257+
dup_annotations = [("@getter", "@deleter"), ("@deleter", "@getter")]
3258+
for dup in dup_annotations:
3259+
with self.subTest(dup=dup):
3260+
block = f"""
3261+
module foo
3262+
class Foo "" ""
3263+
{dup[0]}
3264+
{dup[1]}
3265+
Foo.property
3266+
"""
3267+
expected_error = (f"Can't set {dup[1]}, function is not "
3268+
f"a normal callable")
3269+
self.expect_failure(block, expected_error, lineno=3)
31953270

3196-
def test_deleter_twice(self):
3271+
def test_duplicate_deleter(self):
31973272
block = """
3198-
module foo
3273+
module m
31993274
class Foo "" ""
3200-
@setter
32013275
@deleter
32023276
@deleter
3277+
@setter
32033278
Foo.property
32043279
"""
32053280
expected_error = "Cannot apply @deleter twice to the same function!"
3206-
self.expect_failure(block, expected_error, lineno=4)
3281+
self.expect_failure(block, expected_error, lineno=3)
32073282

32083283
def test_getset_no_class(self):
3209-
for annotation in "@getter", "@setter":
3284+
for annotation in "@getter", "@setter", "@deleter":
32103285
with self.subTest(annotation=annotation):
32113286
block = f"""
32123287
module m
32133288
{annotation}
32143289
m.func
32153290
"""
3216-
expected_error = "@getter and @setter must be methods"
3291+
expected_error = ("@getter, @setter and @deleter "
3292+
"must be methods")
32173293
self.expect_failure(block, expected_error, lineno=2)
32183294

32193295
def test_duplicate_coexist(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Used alone, the ``@deleter`` directive in Argument Clinic now defines a
2+
separate function which deletes the attribute.
3+
It can be combined with a ``@setter`` defined in a separate block: the setter
4+
slot of the :c:type:`PyGetSetDef` entry is then filled with a function which
5+
dispatches to one of them.

0 commit comments

Comments
 (0)