Skip to content

Commit 8d93465

Browse files
gh-113318: Fix @Getter and @Setter in Argument Clinic
Fix generating an accessor in a preprocessor conditional block. Reject the accessors of the same attribute with different C basenames and the same accessor defined twice. Reject deletion of the attribute, which crashed the setter, unless the new directive @deleter is applied to it.
1 parent c72ea53 commit 8d93465

19 files changed

Lines changed: 414 additions & 59 deletions

File tree

Lib/test/clinic.test.c

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5431,14 +5431,53 @@ Test_property_set(PyObject *self, PyObject *value, void *Py_UNUSED(context))
54315431
{
54325432
int return_value;
54335433

5434+
if (value == NULL) {
5435+
PyErr_Format(PyExc_AttributeError,
5436+
"attribute 'property' of '%.100s' objects cannot be deleted",
5437+
Py_TYPE(self)->tp_name);
5438+
return -1;
5439+
}
54345440
return_value = Test_property_set_impl((TestObj *)self, value);
54355441

54365442
return return_value;
54375443
}
54385444

54395445
static int
54405446
Test_property_set_impl(TestObj *self, PyObject *value)
5441-
/*[clinic end generated code: output=49f925ab2a33b637 input=3bc3f46a23c83a88]*/
5447+
/*[clinic end generated code: output=ec103a151cf51d25 input=3bc3f46a23c83a88]*/
5448+
5449+
/*[clinic input]
5450+
@setter
5451+
@deleter
5452+
Test.settable_and_deletable
5453+
[clinic start generated code]*/
5454+
5455+
#if !defined(Test_settable_and_deletable_DOCSTR)
5456+
# define Test_settable_and_deletable_DOCSTR NULL
5457+
#endif
5458+
#if defined(TEST_SETTABLE_AND_DELETABLE_GETSETDEF)
5459+
# undef TEST_SETTABLE_AND_DELETABLE_GETSETDEF
5460+
# 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},
5461+
#else
5462+
# define TEST_SETTABLE_AND_DELETABLE_GETSETDEF {"settable_and_deletable", NULL, (setter)Test_settable_and_deletable_set, NULL},
5463+
#endif
5464+
5465+
static int
5466+
Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value);
5467+
5468+
static int
5469+
Test_settable_and_deletable_set(PyObject *self, PyObject *value, void *Py_UNUSED(context))
5470+
{
5471+
int return_value;
5472+
5473+
return_value = Test_settable_and_deletable_set_impl((TestObj *)self, value);
5474+
5475+
return return_value;
5476+
}
5477+
5478+
static int
5479+
Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value)
5480+
/*[clinic end generated code: output=479986d499b2f56d input=f5647f3511b9daea]*/
54425481

54435482
/*[clinic input]
54445483
@setter
@@ -5463,14 +5502,20 @@ Test_setter_first_with_docstr_set(PyObject *self, PyObject *value, void *Py_UNUS
54635502
{
54645503
int return_value;
54655504

5505+
if (value == NULL) {
5506+
PyErr_Format(PyExc_AttributeError,
5507+
"attribute 'setter_first_with_docstr' of '%.100s' objects cannot be deleted",
5508+
Py_TYPE(self)->tp_name);
5509+
return -1;
5510+
}
54665511
return_value = Test_setter_first_with_docstr_set_impl((TestObj *)self, value);
54675512

54685513
return return_value;
54695514
}
54705515

54715516
static int
54725517
Test_setter_first_with_docstr_set_impl(TestObj *self, PyObject *value)
5473-
/*[clinic end generated code: output=5aaf44373c0af545 input=31a045ce11bbe961]*/
5518+
/*[clinic end generated code: output=eac8bafcaa50aa51 input=31a045ce11bbe961]*/
54745519

54755520
/*[clinic input]
54765521
@getter

Lib/test/test_clinic.py

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,102 @@ def test_ignore_preprocessor_in_comments(self):
794794
""")
795795
self.clinic.parse(raw)
796796

797+
def test_getset_in_ifdef(self):
798+
block = """
799+
/*[clinic input]
800+
output everything block
801+
class Foo "FooObject *" "&Foo_Type"
802+
[clinic start generated code]*/
803+
#ifdef CONDITION
804+
/*[clinic input]
805+
@getter
806+
Foo.property
807+
[clinic start generated code]*/
808+
/*[clinic input]
809+
@setter
810+
Foo.property
811+
[clinic start generated code]*/
812+
#endif
813+
"""
814+
generated = self.clinic.parse(dedent(block))
815+
self.assertIn("#if defined(CONDITION)", generated)
816+
# The getset is undefined if the condition is false.
817+
self.assertIn("#ifndef FOO_PROPERTY_GETSETDEF\n"
818+
" #define FOO_PROPERTY_GETSETDEF\n"
819+
"#endif /* !defined(FOO_PROPERTY_GETSETDEF) */",
820+
generated)
821+
822+
def test_getset_duplicate(self):
823+
for annotation in "@getter", "@setter":
824+
with self.subTest(annotation=annotation):
825+
self.clinic = _make_clinic(filename="test.c")
826+
block = f"""
827+
/*[clinic input]
828+
class Foo "FooObject *" "&Foo_Type"
829+
[clinic start generated code]*/
830+
/*[clinic input]
831+
{annotation}
832+
Foo.property
833+
[clinic start generated code]*/
834+
/*[clinic input]
835+
{annotation}
836+
Foo.property
837+
[clinic start generated code]*/
838+
"""
839+
kind = 'setter' if annotation == '@setter' else 'getter'
840+
err = f"Cannot apply @{kind} to 'Foo.property' twice"
841+
self.expect_failure(block, err, lineno=10)
842+
843+
def test_getset_different_c_basename(self):
844+
block = """
845+
/*[clinic input]
846+
class Foo "FooObject *" "&Foo_Type"
847+
[clinic start generated code]*/
848+
/*[clinic input]
849+
@getter
850+
Foo.property as foo_get
851+
[clinic start generated code]*/
852+
/*[clinic input]
853+
@setter
854+
Foo.property as foo_set
855+
[clinic start generated code]*/
856+
"""
857+
err = "The accessors of 'Foo.property' must have the same C basename"
858+
self.expect_failure(block, err, lineno=10)
859+
860+
def test_setter_deletion_check(self):
861+
block = """
862+
/*[clinic input]
863+
output everything block
864+
class Foo "FooObject *" "&Foo_Type"
865+
[clinic start generated code]*/
866+
/*[clinic input]
867+
@setter
868+
Foo.property
869+
[clinic start generated code]*/
870+
"""
871+
generated = self.clinic.parse(dedent(block))
872+
self.assertIn("if (value == NULL) {", generated)
873+
self.assertIn("\"attribute 'property' of '%.100s' objects "
874+
"cannot be deleted\"", generated)
875+
876+
def test_deleter(self):
877+
# @deleter means that the setter is called with NULL to delete
878+
# the attribute, so it checks the value itself.
879+
block = """
880+
/*[clinic input]
881+
output everything block
882+
class Foo "FooObject *" "&Foo_Type"
883+
[clinic start generated code]*/
884+
/*[clinic input]
885+
@setter
886+
@deleter
887+
Foo.property
888+
[clinic start generated code]*/
889+
"""
890+
generated = self.clinic.parse(dedent(block))
891+
self.assertNotIn("if (value == NULL) {", generated)
892+
797893
def test_var_keyword_non_dict(self):
798894
err = "'var_keyword_object' is not a valid converter"
799895
block = """
@@ -2671,7 +2767,7 @@ class Foo "" ""
26712767
{annotation}
26722768
Foo.property -> int
26732769
"""
2674-
expected_error = f"{annotation} method cannot define a return type"
2770+
expected_error = "@getter and @setter methods cannot define a return type"
26752771
self.expect_failure(block, expected_error, lineno=3)
26762772

26772773
block = f"""
@@ -2682,7 +2778,7 @@ class Foo "" ""
26822778
obj: int
26832779
/
26842780
"""
2685-
expected_error = f"{annotation} methods cannot define parameters"
2781+
expected_error = "@getter and @setter methods cannot define parameters"
26862782
self.expect_failure(block, expected_error)
26872783

26882784
def test_setter_docstring(self):
@@ -2725,9 +2821,51 @@ class Foo "" ""
27252821
{dup[1]}
27262822
Foo.property -> int
27272823
"""
2728-
expected_error = "Cannot apply both @getter and @setter to the same function!"
2824+
expected_error = (f"Can't set {dup[1]}, "
2825+
f"function is not a normal callable")
27292826
self.expect_failure(block, expected_error, lineno=3)
27302827

2828+
def test_deleter_without_setter(self):
2829+
block = """
2830+
module foo
2831+
class Foo "" ""
2832+
@deleter
2833+
Foo.property
2834+
"""
2835+
expected_error = "Can't set @deleter, @setter is not applied"
2836+
self.expect_failure(block, expected_error, lineno=2)
2837+
2838+
block = """
2839+
module foo
2840+
class Foo "" ""
2841+
@deleter
2842+
@setter
2843+
Foo.property
2844+
"""
2845+
self.expect_failure(block, expected_error, lineno=2)
2846+
2847+
def test_deleter_twice(self):
2848+
block = """
2849+
module foo
2850+
class Foo "" ""
2851+
@setter
2852+
@deleter
2853+
@deleter
2854+
Foo.property
2855+
"""
2856+
expected_error = "Cannot apply @deleter twice to the same function!"
2857+
self.expect_failure(block, expected_error, lineno=4)
2858+
2859+
def test_setter_and_deleter(self):
2860+
function = self.parse_function("""
2861+
module foo
2862+
class Foo "" ""
2863+
@setter
2864+
@deleter
2865+
Foo.property
2866+
""", signatures_in_block=3, function_index=2)
2867+
self.assertEqual(function.kind, FunctionKind.SETTER_AND_DELETER)
2868+
27312869
def test_getset_no_class(self):
27322870
for annotation in "@getter", "@setter":
27332871
with self.subTest(annotation=annotation):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix crashes when deleting an attribute whose setter is generated by Argument
2+
Clinic and is not prepared for deletion, among them
3+
:attr:`frame.f_trace_opcodes` and the ``context``, ``owner`` and ``session``
4+
attributes of ``_ssl._SSLSocket``.
5+
Deleting such attribute now raises :exc:`AttributeError`.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix Argument Clinic for ``@getter`` and ``@setter`` in a preprocessor
2+
conditional block.
3+
It failed with an internal error.
4+
Argument Clinic now also rejects the accessors of the same attribute with
5+
different C basenames, and the same accessor defined twice, which silently
6+
generated invalid or duplicated entries of :c:type:`PyGetSetDef`.

Modules/_asynciomodule.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,13 +1372,14 @@ _asyncio_Future__asyncio_future_blocking_get_impl(FutureObj *self)
13721372
/*[clinic input]
13731373
@critical_section
13741374
@setter
1375+
@deleter
13751376
_asyncio.Future._asyncio_future_blocking
13761377
[clinic start generated code]*/
13771378

13781379
static int
13791380
_asyncio_Future__asyncio_future_blocking_set_impl(FutureObj *self,
13801381
PyObject *value)
1381-
/*[clinic end generated code: output=0686d1cb024a7453 input=3fd4a5f95df788b7]*/
1382+
/*[clinic end generated code: output=0686d1cb024a7453 input=68cea090c8793dd4]*/
13821383

13831384
{
13841385
if (future_ensure_alive(self)) {
@@ -1420,12 +1421,13 @@ _asyncio_Future__log_traceback_get_impl(FutureObj *self)
14201421
/*[clinic input]
14211422
@critical_section
14221423
@setter
1424+
@deleter
14231425
_asyncio.Future._log_traceback
14241426
[clinic start generated code]*/
14251427

14261428
static int
14271429
_asyncio_Future__log_traceback_set_impl(FutureObj *self, PyObject *value)
1428-
/*[clinic end generated code: output=9ce8e19504f42f54 input=30ac8217754b08c2]*/
1430+
/*[clinic end generated code: output=9ce8e19504f42f54 input=469dbdd15343d39f]*/
14291431
{
14301432
if (value == NULL) {
14311433
PyErr_SetString(PyExc_AttributeError, "cannot delete attribute");
@@ -1585,12 +1587,13 @@ _asyncio_Future__cancel_message_get_impl(FutureObj *self)
15851587
/*[clinic input]
15861588
@critical_section
15871589
@setter
1590+
@deleter
15881591
_asyncio.Future._cancel_message
15891592
[clinic start generated code]*/
15901593

15911594
static int
15921595
_asyncio_Future__cancel_message_set_impl(FutureObj *self, PyObject *value)
1593-
/*[clinic end generated code: output=0854b2f77bff2209 input=f461d17f2d891fad]*/
1596+
/*[clinic end generated code: output=0854b2f77bff2209 input=68b3a24731dfb629]*/
15941597
{
15951598
if (value == NULL) {
15961599
PyErr_SetString(PyExc_AttributeError, "cannot delete attribute");
@@ -2443,12 +2446,13 @@ _asyncio_Task__log_destroy_pending_get_impl(TaskObj *self)
24432446
/*[clinic input]
24442447
@critical_section
24452448
@setter
2449+
@deleter
24462450
_asyncio.Task._log_destroy_pending
24472451
[clinic start generated code]*/
24482452

24492453
static int
24502454
_asyncio_Task__log_destroy_pending_set_impl(TaskObj *self, PyObject *value)
2451-
/*[clinic end generated code: output=7ebc030bb92ec5ce input=49b759c97d1216a4]*/
2455+
/*[clinic end generated code: output=7ebc030bb92ec5ce input=31af83e8bf57ac6f]*/
24522456
{
24532457
if (value == NULL) {
24542458
PyErr_SetString(PyExc_AttributeError, "cannot delete attribute");

0 commit comments

Comments
 (0)