Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion Lib/test/clinic.test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
168 changes: 122 additions & 46 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, '
Expand Down Expand Up @@ -962,21 +961,76 @@ 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]*/
"""
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")
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -3050,8 +3129,8 @@ class Foo "" ""
block = """
module m
class Foo "" ""
@setter
@deleter
@setter
Foo.property
value: object
"""
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading