diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 0274edd384da43..8d2aced0b9caaf 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -15,6 +15,7 @@ import re import sys import unittest +from unittest import mock import warnings test_tools.skip_if_missing('clinic') @@ -22,7 +23,8 @@ import libclinic from libclinic import ClinicError, unspecified, NULL, fail from libclinic.converters import ( - int_converter, object_converter, str_converter, self_converter) + int_converter, object_converter, str_converter, self_converter, + Py_ssize_t_converter) from libclinic.function import ( Module, Class, Function, FunctionKind, Parameter, permute_optional_groups, permute_right_option_groups, @@ -34,6 +36,7 @@ from libclinic.block_parser import Block, BlockParser from libclinic.codegen import BlockPrinter, Destination from libclinic.dsl_parser import DSLParser + from libclinic.slots import SlotSpec, SLOTS from libclinic.cli import parse_file, Clinic @@ -807,6 +810,112 @@ def test_var_keyword_non_dict(self): """ self.expect_failure(block, err, lineno=4) + def test_slot_next(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + Foo.__next__ + [clinic start generated code]*/ + /*[clinic input] + @critical_section + Foo.__next__ as foo_iternext + [clinic start generated code]*/ + #ifdef CONDITION + /*[clinic input] + Foo.__next__ as foo_iternext_cond + [clinic start generated code]*/ + #endif + """ + generated = self.clinic.parse(dedent(block)) + self.assertIn(dedent(""" + static PyObject * + Foo_next(PyObject *self) + { + return Foo_next_impl((FooObject *)self); + } + """), generated) + self.assertIn(dedent(""" + static PyObject * + foo_iternext(PyObject *self) + { + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = foo_iternext_impl((FooObject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; + } + """), generated) + self.assertIn("#if defined(CONDITION)\n\nstatic PyObject *\n" + "foo_iternext_cond_impl(FooObject *self);", generated) + # A slot function is not stored in a PyMethodDef. + for text in "METHODDEF", "PyDoc_", "#ifndef": + self.assertNotIn(text, generated) + + def test_slot_with_parameters_and_return_type(self): + # Slots which take arguments or return something else than an + # object are not supported yet; check the machinery with fake ones. + slots = { + '__contains__': SlotSpec('__contains__', 'int', 'int', + {(object_converter,): 'sq_contains'}), + '__bool__': SlotSpec('__bool__', 'int', 'int', {(): 'nb_bool'}), + } + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + Foo.__contains__ + value: object + / + [clinic start generated code]*/ + /*[clinic input] + @critical_section + Foo.__bool__ + [clinic start generated code]*/ + """ + with mock.patch.dict(SLOTS, slots): + generated = self.clinic.parse(dedent(block)) + self.assertIn(dedent(""" + static int + Foo_contains(PyObject *self, PyObject *value) + { + return Foo_contains_impl((FooObject *)self, value); + } + """), generated) + self.assertIn(dedent(""" + static int + Foo_bool(PyObject *self) + { + int return_value = -1; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = Foo_bool_impl((FooObject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; + } + """), generated) + + def test_slot_cannot_be_cloned(self): + block = """ + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + Foo.__next__ + [clinic start generated code]*/ + /*[clinic input] + Foo.other = Foo.__next__ + [clinic start generated code]*/ + """ + self.expect_failure(block, "Type slot methods cannot be cloned", lineno=8) + def test_getset_in_ifdef(self): block = """ /*[clinic input] @@ -3237,6 +3346,78 @@ class Foo "" "" f"a normal callable") self.expect_failure(block, expected_error, lineno=3) + def test_slot_next(self): + function = self.parse_function(""" + module foo + class Foo "FooObject *" "" + Foo.__next__ + """, signatures_in_block=3, function_index=2) + self.assertIs(function.kind, FunctionKind.SLOT) + self.assertEqual(function.slot.name, "__next__") + self.assertEqual(function.c_basename, "Foo_next") + self.assertEqual(function.return_converter.type, "PyObject *") + self.assertEqual(list(function.parameters), ["self"]) + + def test_slot_parameters(self): + slot = SlotSpec('__contains__', 'int', 'int', { + (Py_ssize_t_converter,): 'sq_fake', + (object_converter,): 'sq_contains'}) + block = """ + module foo + class Foo "FooObject *" "" + Foo.__contains__ + value: {converter} + / + """ + errors = [ + ("Foo.__contains__", + "'__contains__' must declare (Py_ssize_t) for sq_fake or " + "(object) for sq_contains; got ()"), + ("Foo.__contains__\n value: int\n /", + "'__contains__' must declare"), + ("Foo.__contains__\n value: object", + "The parameters of '__contains__' must be positional-only"), + ("Foo.__contains__\n value: object = None\n /", + "The parameters of '__contains__' cannot have a default value"), + ] + with mock.patch.dict(SLOTS, {'__contains__': slot}): + for converter in "Py_ssize_t", "object": + with self.subTest(converter=converter): + function = self.parse_function( + block.format(converter=converter), + signatures_in_block=3, function_index=2) + self.assertIs(function.kind, FunctionKind.SLOT) + self.assertEqual(function.return_converter.type, "int") + self.assertEqual(list(function.parameters), ["self", "value"]) + for body, err in errors: + with self.subTest(block=body): + body = 'class Foo "FooObject *" "&Foo_Type"\n' + body + self.expect_failure(body, err) + + def test_slot_errors(self): + cases = [ + ("module foo\nfoo.__next__", + "'__next__' must be a method!"), + ("@classmethod\nFoo.__next__", + "'__next__' must be a normal method"), + ("@getter\nFoo.__next__", + "'__next__' must be a normal method"), + ("@coexist\nFoo.__next__", + "@coexist cannot be applied to '__next__'"), + ("@vectorcall\nFoo.__next__", + "@vectorcall can only be used with __init__ and __new__"), + ("Foo.__next__\n a: int", + "'__next__' methods cannot define parameters"), + ("Foo.__next__ -> int", + "'Foo.__next__' cannot define a return type"), + ("Foo.__next__\n\nReturn the next item.", + "'__next__' methods cannot have a docstring"), + ] + for block, err in cases: + with self.subTest(block=block): + block = 'class Foo "FooObject *" "&Foo_Type"\n' + block + self.expect_failure(block, err) + def test_deleter_without_setter(self): block = """ module foo diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-10-20-12-50-00.gh-issue-114258.slotNx.rst b/Misc/NEWS.d/next/Tools-Demos/2026-10-20-12-50-00.gh-issue-114258.slotNx.rst new file mode 100644 index 00000000000000..afaeda209e1bd2 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-10-20-12-50-00.gh-issue-114258.slotNx.rst @@ -0,0 +1,3 @@ +Argument Clinic now supports the ``__next__`` type slot (``tp_iternext``), +including ``@critical_section``. + diff --git a/Modules/_csv.c b/Modules/_csv.c index 34548b94385693..484f7bc0f9d2d4 100644 --- a/Modules/_csv.c +++ b/Modules/_csv.c @@ -22,8 +22,9 @@ module instead. /*[clinic input] module _csv class _csv.Dialect "DialectObj *" "clinic_state()->dialect_type" +class _csv.Reader "ReaderObj *" "clinic_state()->reader_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=042fcc14c7d541cc]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=7bdf66dc7d68ed90]*/ #define NOT_SET ((Py_UCS4)-1) #define EOL ((Py_UCS4)-2) @@ -124,8 +125,6 @@ typedef struct { bool nonascii_special; } DialectObj; -#include "clinic/_csv.c.h" - typedef struct { PyObject_HEAD @@ -161,6 +160,8 @@ typedef struct { #define _ReaderObj_CAST(op) ((ReaderObj *)(op)) #define _WriterObj_CAST(op) ((WriterObj *)(op)) +#include "clinic/_csv.c.h" + /* * DIALECT class */ @@ -1020,11 +1021,15 @@ parse_reset(ReaderObj *self) return 0; } +/*[clinic input] +@critical_section +_csv.Reader.__next__ as Reader_iternext +[clinic start generated code]*/ + static PyObject * -Reader_iternext_lock_held(PyObject *op) +Reader_iternext_impl(ReaderObj *self) +/*[clinic end generated code: output=3a02b0254a76e1fc input=97c70a4e2fe621e4]*/ { - ReaderObj *self = _ReaderObj_CAST(op); - PyObject *fields = NULL; Py_UCS4 c; Py_ssize_t pos, linelen; @@ -1094,16 +1099,6 @@ Reader_iternext_lock_held(PyObject *op) return fields; } -static PyObject * -Reader_iternext(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = Reader_iternext_lock_held(op); - Py_END_CRITICAL_SECTION(); - return result; -} - static void Reader_dealloc(PyObject *op) { diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c index e725e863a0740a..2374a43ef74eab 100644 --- a/Modules/_io/bytesio.c +++ b/Modules/_io/bytesio.c @@ -768,13 +768,18 @@ _io_BytesIO_truncate_impl(bytesio *self, PyObject *size) return PyLong_FromSsize_t(new_size); } +/*[clinic input] +@critical_section +_io.BytesIO.__next__ as bytesio_iternext +[clinic start generated code]*/ + static PyObject * -bytesio_iternext_lock_held(PyObject *op) +bytesio_iternext_impl(bytesio *self) +/*[clinic end generated code: output=f530bdee3a720d48 input=7fb3a763efe11a21]*/ { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); Py_ssize_t n; - bytesio *self = bytesio_CAST(op); CHECK_CLOSED(self); @@ -786,16 +791,6 @@ bytesio_iternext_lock_held(PyObject *op) return read_bytes_lock_held(self, n); } -static PyObject * -bytesio_iternext(PyObject *op) -{ - PyObject *ret; - Py_BEGIN_CRITICAL_SECTION(op); - ret = bytesio_iternext_lock_held(op); - Py_END_CRITICAL_SECTION(); - return ret; -} - /*[clinic input] @critical_section _io.BytesIO.seek diff --git a/Modules/_io/clinic/bytesio.c.h b/Modules/_io/clinic/bytesio.c.h index afdb60e506a2ce..b2305e35ca81e7 100644 --- a/Modules/_io/clinic/bytesio.c.h +++ b/Modules/_io/clinic/bytesio.c.h @@ -511,6 +511,21 @@ _io_BytesIO_truncate(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return return_value; } +static PyObject * +bytesio_iternext_impl(bytesio *self); + +static PyObject * +bytesio_iternext(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = bytesio_iternext_impl((bytesio *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(_io_BytesIO_seek__doc__, "seek($self, pos, whence=0, /)\n" "--\n" @@ -780,4 +795,4 @@ _io_BytesIO___sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored)) } #define _IO_BYTESIO_CLOSED_GETSETDEF {"closed", (getter)_io_BytesIO_closed_get, (setter)NULL, _io_BytesIO_closed__doc__}, -/*[clinic end generated code: output=a3dc7a416dae6ae9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a2bc96c358d6e240 input=a9049054013a1b77]*/ diff --git a/Modules/_io/clinic/stringio.c.h b/Modules/_io/clinic/stringio.c.h index 66b7a57cdf9333..bda87205e6bb71 100644 --- a/Modules/_io/clinic/stringio.c.h +++ b/Modules/_io/clinic/stringio.c.h @@ -135,6 +135,21 @@ _io_StringIO_readline(PyObject *self, PyObject *const *args, Py_ssize_t nargs) return return_value; } +static PyObject * +stringio_iternext_impl(stringio *self); + +static PyObject * +stringio_iternext(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = stringio_iternext_impl((stringio *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(_io_StringIO_truncate__doc__, "truncate($self, pos=None, /)\n" "--\n" @@ -539,4 +554,4 @@ _io_StringIO_newlines_get(PyObject *self, void *Py_UNUSED(context)) #define _IO_STRINGIO_NEWLINES_GETSETDEF {"newlines", (getter)_io_StringIO_newlines_get, (setter)NULL, NULL}, -/*[clinic end generated code: output=4100afa5e4f295d7 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=38a0385529d9cbef input=a9049054013a1b77]*/ diff --git a/Modules/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h index 6d63d4706f609c..51e75d05061799 100644 --- a/Modules/_io/clinic/textio.c.h +++ b/Modules/_io/clinic/textio.c.h @@ -1096,6 +1096,21 @@ _io_TextIOWrapper_close(PyObject *self, PyObject *Py_UNUSED(ignored)) return return_value; } +static PyObject * +textiowrapper_iternext_impl(textio *self); + +static PyObject * +textiowrapper_iternext(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = textiowrapper_iternext_impl((textio *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + static PyObject * _io_TextIOWrapper_name_get_impl(textio *self); @@ -1246,4 +1261,4 @@ _io_TextIOWrapper_buffer_get(PyObject *self, void *Py_UNUSED(context)) #define _IO_TEXTIOWRAPPER_BUFFER_GETSETDEF {"buffer", (getter)_io_TextIOWrapper_buffer_get, (setter)NULL, NULL}, -/*[clinic end generated code: output=89de9ab341df9f66 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=f3dd3fff36cd2142 input=a9049054013a1b77]*/ diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c index 93a82bcc0c6e3e..e9a46167cff596 100644 --- a/Modules/_io/stringio.c +++ b/Modules/_io/stringio.c @@ -420,13 +420,18 @@ _io_StringIO_readline_impl(stringio *self, Py_ssize_t size) return _stringio_readline(self, size); } +/*[clinic input] +@critical_section +_io.StringIO.__next__ as stringio_iternext +[clinic start generated code]*/ + static PyObject * -stringio_iternext_lock_held(PyObject *op) +stringio_iternext_impl(stringio *self) +/*[clinic end generated code: output=51eb3991b1c79c41 input=b6cf3e73fab6b42e]*/ { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyObject *line; - stringio *self = stringio_CAST(op); CHECK_INITIALIZED(self); CHECK_CLOSED(self); @@ -438,7 +443,7 @@ stringio_iternext_lock_held(PyObject *op) } else { /* XXX is subclassing StringIO really supported? */ - line = PyObject_CallMethodNoArgs(op, &_Py_ID(readline)); + line = PyObject_CallMethodNoArgs((PyObject *)self, &_Py_ID(readline)); if (line && !PyUnicode_Check(line)) { PyErr_Format(PyExc_OSError, "readline() should have returned a str object, " @@ -460,16 +465,6 @@ stringio_iternext_lock_held(PyObject *op) return line; } -static PyObject * -stringio_iternext(PyObject *op) -{ - PyObject *ret; - Py_BEGIN_CRITICAL_SECTION(op); - ret = stringio_iternext_lock_held(op); - Py_END_CRITICAL_SECTION(); - return ret; -} - /*[clinic input] @critical_section _io.StringIO.truncate diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 3736179807f45d..f33ab46b10fd2e 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -3287,12 +3287,17 @@ _io_TextIOWrapper_close_impl(textio *self) } } +/*[clinic input] +@critical_section +_io.TextIOWrapper.__next__ as textiowrapper_iternext +[clinic start generated code]*/ + static PyObject * -textiowrapper_iternext_lock_held(PyObject *op) +textiowrapper_iternext_impl(textio *self) +/*[clinic end generated code: output=f365c4287cd43a1f input=c5b4c79faf0c98cb]*/ { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self); PyObject *line; - textio *self = textio_CAST(op); CHECK_ATTACHED(self); @@ -3302,7 +3307,7 @@ textiowrapper_iternext_lock_held(PyObject *op) line = _textiowrapper_readline(self, -1); } else { - line = PyObject_CallMethodNoArgs(op, &_Py_ID(readline)); + line = PyObject_CallMethodNoArgs((PyObject *)self, &_Py_ID(readline)); if (line && !PyUnicode_Check(line)) { PyErr_Format(PyExc_OSError, "readline() should have returned a str object, " @@ -3326,16 +3331,6 @@ textiowrapper_iternext_lock_held(PyObject *op) return line; } -static PyObject * -textiowrapper_iternext(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = textiowrapper_iternext_lock_held(op); - Py_END_CRITICAL_SECTION(); - return result; -} - /*[clinic input] @critical_section @getter diff --git a/Modules/clinic/_csv.c.h b/Modules/clinic/_csv.c.h index b25f30893224cd..df3bdbadf29aed 100644 --- a/Modules/clinic/_csv.c.h +++ b/Modules/clinic/_csv.c.h @@ -6,6 +6,7 @@ preserve # include "pycore_gc.h" // PyGC_Head # include "pycore_runtime.h" // _Py_ID() #endif +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_modsupport.h" // _PyArg_NoPositional() PyDoc_STRVAR(_csv_Dialect___reduce____doc__, @@ -87,6 +88,21 @@ _csv_Dialect___replace__(PyObject *self, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +Reader_iternext_impl(ReaderObj *self); + +static PyObject * +Reader_iternext(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = Reader_iternext_impl((ReaderObj *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(_csv_reader__doc__, "reader($module, iterable, dialect=\'excel\', /, **fmtparams)\n" "--\n" @@ -449,4 +465,4 @@ _csv_field_size_limit(PyObject *module, PyObject *const *args, Py_ssize_t nargs, exit: return return_value; } -/*[clinic end generated code: output=1a90b8a8ed82497d input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7e3a92d524d88023 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/itertoolsmodule.c.h b/Modules/clinic/itertoolsmodule.c.h index d9917b47dc9037..d6365b46f04b26 100644 --- a/Modules/clinic/itertoolsmodule.c.h +++ b/Modules/clinic/itertoolsmodule.c.h @@ -7,6 +7,7 @@ preserve # include "pycore_runtime.h" // _Py_ID() #endif #include "pycore_abstract.h" // _PyNumber_Index() +#include "pycore_critical_section.h"// Py_BEGIN_CRITICAL_SECTION() #include "pycore_modsupport.h" // _PyArg_UnpackKeywords() PyDoc_STRVAR(batched_new__doc__, @@ -508,6 +509,36 @@ itertools_chain_from_iterable(PyObject *type, PyObject *arg) return return_value; } +static PyObject * +chain_next_impl(chainobject *self); + +static PyObject * +chain_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = chain_next_impl((chainobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + +static PyObject * +product_next_impl(productobject *self); + +static PyObject * +product_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = product_next_impl((productobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(itertools_combinations__doc__, "combinations(iterable, r)\n" "--\n" @@ -586,6 +617,21 @@ itertools_combinations(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +combinations_next_impl(combinationsobject *self); + +static PyObject * +combinations_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = combinations_next_impl((combinationsobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(itertools_combinations_with_replacement__doc__, "combinations_with_replacement(iterable, r)\n" "--\n" @@ -665,6 +711,21 @@ itertools_combinations_with_replacement(PyTypeObject *type, PyObject *args, PyOb return return_value; } +static PyObject * +cwr_next_impl(cwrobject *self); + +static PyObject * +cwr_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = cwr_next_impl((cwrobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(itertools_permutations__doc__, "permutations(iterable, r=None)\n" "--\n" @@ -732,6 +793,21 @@ itertools_permutations(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +permutations_next_impl(permutationsobject *self); + +static PyObject * +permutations_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = permutations_next_impl((permutationsobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(itertools_accumulate__doc__, "accumulate(iterable, func=None, *, initial=None)\n" "--\n" @@ -808,6 +884,21 @@ itertools_accumulate(PyTypeObject *type, PyObject *args, PyObject *kwargs) return return_value; } +static PyObject * +accumulate_next_impl(accumulateobject *self); + +static PyObject * +accumulate_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = accumulate_next_impl((accumulateobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(itertools_compress__doc__, "compress(data, selectors)\n" "--\n" @@ -980,4 +1071,19 @@ itertools_count(PyTypeObject *type, PyObject *args, PyObject *kwargs) exit: return return_value; } -/*[clinic end generated code: output=a34a31f60100e0ff input=a9049054013a1b77]*/ + +static PyObject * +zip_longest_next_impl(ziplongestobject *self); + +static PyObject * +zip_longest_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = zip_longest_next_impl((ziplongestobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} +/*[clinic end generated code: output=4b55088ac24582b7 input=a9049054013a1b77]*/ diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c index c0023c839ca7fe..636b28fa06ef2e 100644 --- a/Modules/itertoolsmodule.c +++ b/Modules/itertoolsmodule.c @@ -65,6 +65,16 @@ find_state_by_type(PyTypeObject *tp) return get_module_state(mod); } +// The generated code (clinic/itertoolsmodule.c.h) needs the object types +// of the type slots implemented with Argument Clinic. +typedef struct chainobject chainobject; +typedef struct productobject productobject; +typedef struct combinationsobject combinationsobject; +typedef struct cwrobject cwrobject; +typedef struct permutationsobject permutationsobject; +typedef struct accumulateobject accumulateobject; +typedef struct ziplongestobject ziplongestobject; + /*[clinic input] module itertools class itertools.groupby "groupbyobject *" "clinic_state()->groupby_type" @@ -77,16 +87,18 @@ class itertools.dropwhile "dropwhileobject *" "clinic_state()->dropwhile_type" class itertools.takewhile "takewhileobject *" "clinic_state()->takewhile_type" class itertools.starmap "starmapobject *" "clinic_state()->starmap_type" class itertools.chain "chainobject *" "clinic_state()->chain_type" +class itertools.product "productobject *" "clinic_state()->product_type" class itertools.combinations "combinationsobject *" "clinic_state()->combinations_type" -class itertools.combinations_with_replacement "cwr_object *" "clinic_state()->cwr_type" +class itertools.combinations_with_replacement "cwrobject *" "clinic_state()->cwr_type" class itertools.permutations "permutationsobject *" "clinic_state()->permutations_type" class itertools.accumulate "accumulateobject *" "clinic_state()->accumulate_type" +class itertools.zip_longest "ziplongestobject *" "clinic_state()->ziplongest_type" class itertools.compress "compressobject *" "clinic_state()->compress_type" class itertools.filterfalse "filterfalseobject *" "clinic_state()->filterfalse_type" class itertools.count "countobject *" "clinic_state()->count_type" class itertools.pairwise "pairwiseobject *" "clinic_state()->pairwise_type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=aa48fe4de9d4080f]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=1086fb2d308d75d2]*/ #define clinic_state() (find_state_by_type(type)) #define clinic_state_by_cls() (get_module_state_by_cls(base_tp)) @@ -1877,11 +1889,11 @@ static PyType_Spec starmap_spec = { /* chain object **************************************************************/ -typedef struct { +struct chainobject { PyObject_HEAD PyObject *source; /* Iterator over input iterables */ PyObject *active; /* Currently running input iterator */ -} chainobject; +}; #define chainobject_CAST(op) ((chainobject *)(op)) @@ -1963,10 +1975,16 @@ chain_traverse(PyObject *op, visitproc visit, void *arg) return 0; } -static inline PyObject * -chain_next_lock_held(PyObject *op) +/*[clinic input] +@critical_section +itertools.chain.__next__ as chain_next +[clinic start generated code]*/ + +static PyObject * +chain_next_impl(chainobject *self) +/*[clinic end generated code: output=91e716f45def7fa9 input=e17098e9dc88706b]*/ { - chainobject *lz = chainobject_CAST(op); + chainobject *lz = self; PyObject *item; /* lz->source is the iterator of iterables. If it's NULL, we've already @@ -2002,16 +2020,6 @@ chain_next_lock_held(PyObject *op) return NULL; } -static PyObject * -chain_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = chain_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - PyDoc_STRVAR(chain_doc, "chain(*iterables)\n\ --\n\ @@ -2055,13 +2063,13 @@ static PyType_Spec chain_spec = { /* product object ************************************************************/ -typedef struct { +struct productobject { PyObject_HEAD PyObject *pools; /* tuple of pool tuples */ Py_ssize_t *indices; /* one index per pool */ PyObject *result; /* most recently returned result tuple */ int stopped; /* set to 1 when the iterator is exhausted */ -} productobject; +}; #define productobject_CAST(op) ((productobject *)(op)) @@ -2182,10 +2190,16 @@ product_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.product.__next__ as product_next +[clinic start generated code]*/ + static PyObject * -product_next_lock_held(PyObject *op) +product_next_impl(productobject *self) +/*[clinic end generated code: output=d20b8571608c2043 input=54266fe7ed4a857a]*/ { - productobject *lz = productobject_CAST(op); + productobject *lz = self; PyObject *pool; PyObject *elem; PyObject *oldelem; @@ -2269,16 +2283,6 @@ product_next_lock_held(PyObject *op) return NULL; } -static PyObject * -product_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = product_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - static PyMethodDef product_methods[] = { {"__sizeof__", product_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ @@ -2323,14 +2327,14 @@ static PyType_Spec product_spec = { /* combinations object *******************************************************/ -typedef struct { +struct combinationsobject { PyObject_HEAD PyObject *pool; /* input converted to a tuple */ Py_ssize_t *indices; /* one index per result element */ PyObject *result; /* most recently returned result tuple */ Py_ssize_t r; /* size of result tuple */ int stopped; /* set to 1 when the iterator is exhausted */ -} combinationsobject; +}; #define combinationsobject_CAST(op) ((combinationsobject *)(op)) @@ -2421,10 +2425,16 @@ combinations_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.combinations.__next__ as combinations_next +[clinic start generated code]*/ + static PyObject * -combinations_next_lock_held(PyObject *op) +combinations_next_impl(combinationsobject *self) +/*[clinic end generated code: output=19965c1a1f4b8f2a input=93000f6ec48380d7]*/ { - combinationsobject *co = combinationsobject_CAST(op); + combinationsobject *co = self; PyObject *elem; PyObject *oldelem; PyObject *pool = co->pool; @@ -2507,16 +2517,6 @@ combinations_next_lock_held(PyObject *op) return NULL; } -static PyObject * -combinations_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = combinations_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - static PyMethodDef combinations_methods[] = { {"__sizeof__", combinations_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ @@ -2572,14 +2572,14 @@ static PyType_Spec combinations_spec = { if sorted(indices) == list(indices): yield tuple(pool[i] for i in indices) */ -typedef struct { +struct cwrobject { PyObject_HEAD PyObject *pool; /* input converted to a tuple */ Py_ssize_t *indices; /* one index per result element */ PyObject *result; /* most recently returned result tuple */ Py_ssize_t r; /* size of result tuple */ int stopped; /* set to 1 when the cwr iterator is exhausted */ -} cwrobject; +}; #define cwrobject_CAST(op) ((cwrobject *)(op)) @@ -2673,10 +2673,16 @@ cwr_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.combinations_with_replacement.__next__ as cwr_next +[clinic start generated code]*/ + static PyObject * -cwr_next_lock_held(PyObject *op) +cwr_next_impl(cwrobject *self) +/*[clinic end generated code: output=760ddcfbf905ee37 input=d9ed46a28a5bb29a]*/ { - cwrobject *co = cwrobject_CAST(op); + cwrobject *co = self; PyObject *elem; PyObject *oldelem; PyObject *pool = co->pool; @@ -2753,16 +2759,6 @@ cwr_next_lock_held(PyObject *op) return NULL; } -static PyObject * -cwr_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = cwr_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - static PyMethodDef cwr_methods[] = { {"__sizeof__", cwr_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ @@ -2818,7 +2814,7 @@ def permutations(iterable, r=None): return */ -typedef struct { +struct permutationsobject { PyObject_HEAD PyObject *pool; /* input converted to a tuple */ Py_ssize_t *indices; /* one index per element in the pool */ @@ -2826,7 +2822,7 @@ typedef struct { PyObject *result; /* most recently returned result tuple */ Py_ssize_t r; /* size of result tuple */ int stopped; /* set to 1 when the iterator is exhausted */ -} permutationsobject; +}; #define permutationsobject_CAST(op) ((permutationsobject *)(op)) @@ -2942,10 +2938,16 @@ permutations_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.permutations.__next__ as permutations_next +[clinic start generated code]*/ + static PyObject * -permutations_next_lock_held(PyObject *op) +permutations_next_impl(permutationsobject *self) +/*[clinic end generated code: output=17d8db4465bd3ee5 input=d4c1a1eed76a35e0]*/ { - permutationsobject *po = permutationsobject_CAST(op); + permutationsobject *po = self; PyObject *elem; PyObject *oldelem; PyObject *pool = po->pool; @@ -3033,16 +3035,6 @@ permutations_next_lock_held(PyObject *op) return NULL; } -static PyObject * -permutations_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = permutations_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - static PyMethodDef permuations_methods[] = { {"__sizeof__", permutations_sizeof, METH_NOARGS, sizeof_doc}, {NULL, NULL} /* sentinel */ @@ -3072,14 +3064,14 @@ static PyType_Spec permutations_spec = { /* accumulate object ********************************************************/ -typedef struct { +struct accumulateobject { PyObject_HEAD PyObject *total; PyObject *it; PyObject *binop; PyObject *initial; itertools_state *state; -} accumulateobject; +}; #define accumulateobject_CAST(op) ((accumulateobject *)(op)) @@ -3149,10 +3141,16 @@ accumulate_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.accumulate.__next__ as accumulate_next +[clinic start generated code]*/ + static PyObject * -accumulate_next_lock_held(PyObject *op) +accumulate_next_impl(accumulateobject *self) +/*[clinic end generated code: output=30d3f9b40248d8a6 input=758a144b7cfb1ef4]*/ { - accumulateobject *lz = accumulateobject_CAST(op); + accumulateobject *lz = self; PyObject *val, *newtotal; if (lz->initial != Py_None) { @@ -3182,16 +3180,6 @@ accumulate_next_lock_held(PyObject *op) return newtotal; } -static PyObject * -accumulate_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = accumulate_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - static PyType_Slot accumulate_slots[] = { {Py_tp_dealloc, accumulate_dealloc}, {Py_tp_getattro, PyObject_GenericGetAttr}, @@ -3852,14 +3840,14 @@ static PyType_Spec repeat_spec = { /* ziplongest object *********************************************************/ -typedef struct { +struct ziplongestobject { PyObject_HEAD Py_ssize_t tuplesize; Py_ssize_t numactive; PyObject *ittuple; /* tuple of iterators */ PyObject *result; PyObject *fillvalue; -} ziplongestobject; +}; #define ziplongestobject_CAST(op) ((ziplongestobject *)(op)) @@ -3955,10 +3943,16 @@ zip_longest_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +/*[clinic input] +@critical_section +itertools.zip_longest.__next__ as zip_longest_next +[clinic start generated code]*/ + static PyObject * -zip_longest_next_lock_held(PyObject *op) +zip_longest_next_impl(ziplongestobject *self) +/*[clinic end generated code: output=11bc756cf896c576 input=5b1e3b52098852a9]*/ { - ziplongestobject *lz = ziplongestobject_CAST(op); + ziplongestobject *lz = self; Py_ssize_t i; Py_ssize_t tuplesize = lz->tuplesize; PyObject *result = lz->result; @@ -4027,16 +4021,6 @@ zip_longest_next_lock_held(PyObject *op) return result; } -static PyObject * -zip_longest_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = zip_longest_next_lock_held(op); - Py_END_CRITICAL_SECTION() - return result; -} - PyDoc_STRVAR(zip_longest_doc, "zip_longest(*iterables, fillvalue=None)\n\ --\n\ diff --git a/Objects/clinic/odictobject.c.h b/Objects/clinic/odictobject.c.h index 92129e6444810f..ebef5dad597c54 100644 --- a/Objects/clinic/odictobject.c.h +++ b/Objects/clinic/odictobject.c.h @@ -452,4 +452,19 @@ OrderedDict_move_to_end(PyObject *self, PyObject *const *args, Py_ssize_t nargs, exit: return return_value; } -/*[clinic end generated code: output=89f7e92de998f9a4 input=a9049054013a1b77]*/ + +static PyObject * +odictiter_iternext_impl(PyObject *self); + +static PyObject * +odictiter_iternext(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = odictiter_iternext_impl(self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} +/*[clinic end generated code: output=b729b3f1d40afcd9 input=a9049054013a1b77]*/ diff --git a/Objects/clinic/rangeobject.c.h b/Objects/clinic/rangeobject.c.h index d9142eddde4b17..1cdc2e43b50dbc 100644 --- a/Objects/clinic/rangeobject.c.h +++ b/Objects/clinic/rangeobject.c.h @@ -147,4 +147,19 @@ longrange_iterator___setstate__(PyObject *r, PyObject *state) return return_value; } -/*[clinic end generated code: output=719c0e4c81fe0f4a input=a9049054013a1b77]*/ + +static PyObject * +longrangeiter_next_impl(longrangeiterobject *self); + +static PyObject * +longrangeiter_next(PyObject *self) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = longrangeiter_next_impl((longrangeiterobject *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} +/*[clinic end generated code: output=e31cc65008f9db4f input=a9049054013a1b77]*/ diff --git a/Objects/odictobject.c b/Objects/odictobject.c index a860ade3b148e5..b9283ad6469d08 100644 --- a/Objects/odictobject.c +++ b/Objects/odictobject.c @@ -478,8 +478,9 @@ Potential Optimizations /*[clinic input] class OrderedDict "PyODictObject *" "&PyODict_Type" +class odictiter "PyObject *" "&PyODictIter_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ca0641cf6143d4af]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=ae5599eeb14d29ae]*/ typedef struct _odictnode _ODictNode; @@ -1804,10 +1805,16 @@ odictiter_nextkey(odictiterobject *di) return res; } +/*[clinic input] +@critical_section +odictiter.__next__ as odictiter_iternext +[clinic start generated code]*/ + static PyObject * -odictiter_iternext_lock_held(PyObject *op) +odictiter_iternext_impl(PyObject *self) +/*[clinic end generated code: output=71aef8eec9e1a133 input=bc8bec6c85481b93]*/ { - odictiterobject *di = (odictiterobject*)op; + odictiterobject *di = (odictiterobject*)self; PyObject *result, *value; PyObject *key = odictiter_nextkey(di); /* new reference */ @@ -1862,16 +1869,6 @@ odictiter_iternext_lock_held(PyObject *op) return NULL; } -static PyObject * -odictiter_iternext(PyObject *op) -{ - PyObject *res; - Py_BEGIN_CRITICAL_SECTION(op); - res = odictiter_iternext_lock_held(op); - Py_END_CRITICAL_SECTION(); - return res; -} - /* No need for tp_clear because odictiterobject is not mutable. */ diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c index 55b7f108730728..2279d4ce17d2f6 100644 --- a/Objects/rangeobject.c +++ b/Objects/rangeobject.c @@ -1158,10 +1158,16 @@ longrangeiter_dealloc(PyObject *op) PyObject_Free(r); } +/*[clinic input] +@critical_section +longrange_iterator.__next__ as longrangeiter_next +[clinic start generated code]*/ + static PyObject * -longrangeiter_next_lock_held(PyObject *op) +longrangeiter_next_impl(longrangeiterobject *self) +/*[clinic end generated code: output=49bf9a9b89c07ba5 input=2c4ac537c23a92d3]*/ { - longrangeiterobject *r = (longrangeiterobject*)op; + longrangeiterobject *r = self; if (PyObject_RichCompareBool(r->len, _PyLong_GetZero(), Py_GT) != 1) return NULL; @@ -1180,16 +1186,6 @@ longrangeiter_next_lock_held(PyObject *op) return result; } -static PyObject * -longrangeiter_next(PyObject *op) -{ - PyObject *result; - Py_BEGIN_CRITICAL_SECTION(op); - result = longrangeiter_next_lock_held(op); - Py_END_CRITICAL_SECTION(); - return result; -} - PyTypeObject PyLongRangeIter_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) "longrange_iterator", /* tp_name */ diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index 1f71c6cda81b81..f8ab2fd2515461 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -12,7 +12,7 @@ from libclinic.function import ( Module, Class, Property, Function, Parameter, group_to_variable_name, - METHOD_INIT, METHOD_NEW, + METHOD_INIT, METHOD_NEW, SLOT, ACCESSORS, SETTERS) from libclinic.converters import self_converter from libclinic.parse_args import ( @@ -383,6 +383,10 @@ def render_function( if ("METH_O" in templates['methoddef_define'] and '{parser_parameters}' in templates['parser_prototype']): data.declarations.pop(0) + # A slot function receives its arguments as parameters, see + # parse_slot(): there is nothing to declare or parse. + if f.kind is SLOT: + data.declarations.clear() full_name = f.full_name template_dict = {'full_name': full_name} @@ -404,12 +408,17 @@ def render_function( for converter in converters: converter.set_template_dict(template_dict) - if f.kind not in SETTERS | {METHOD_INIT}: + if f.kind not in SETTERS | {METHOD_INIT, SLOT}: f.return_converter.render(f, data) template_dict['impl_return_type'] = f.return_converter.type - # tp_init returns int; every other parser returns an object. - template_dict['return_type'] = ( - 'int' if f.kind is METHOD_INIT else 'PyObject *') + # tp_init returns int, a slot function returns what its slot + # declares, every other parser returns an object. + if f.kind is SLOT: + assert f.slot is not None + template_dict['return_type'] = f.slot.return_type + else: + template_dict['return_type'] = ( + 'int' if f.kind is METHOD_INIT else 'PyObject *') template_dict['declarations'] = libclinic.format_escape("\n".join(data.declarations)) template_dict['initializers'] = "\n\n".join(data.initializers) diff --git a/Tools/clinic/libclinic/converter.py b/Tools/clinic/libclinic/converter.py index 0caa8a8956f999..2f9f54f7e29df4 100644 --- a/Tools/clinic/libclinic/converter.py +++ b/Tools/clinic/libclinic/converter.py @@ -275,6 +275,11 @@ def __getattr__(self, attr): else: # pragma: no cover pass + @classmethod + def dsl_name(cls) -> str: + """The name of the converter in the DSL, e.g. 'object'.""" + return cls.__name__.removesuffix('_converter') + def converter_init(self) -> None: pass diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py index 8db4c5fe22e26d..c54dab302b6658 100644 --- a/Tools/clinic/libclinic/converters.py +++ b/Tools/clinic/libclinic/converters.py @@ -8,7 +8,7 @@ from libclinic import fail, NullType, unspecified, NULL, c_bytes_repr, c_unichar_repr from libclinic.function import ( Function, Parameter, - CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, + CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, SLOT, ACCESSORS) from libclinic.codegen import CRenderData, TemplateDict from libclinic.converter import ( @@ -1197,7 +1197,7 @@ def correct_name_for_self( f: Function, parser: bool = False ) -> tuple[str, str]: - if f.kind in {CALLABLE, METHOD_INIT} | ACCESSORS: + if f.kind in {CALLABLE, METHOD_INIT, SLOT} | ACCESSORS: if f.cls: return "PyObject *", "self" return "PyObject *", "module" diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 514844af1d05b7..60730b8828d843 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, SETTER_AND_DELETER, SLOT, ACCESSORS, SETTERS) from libclinic.converter import ( converters, legacy_converters) @@ -28,12 +28,15 @@ from libclinic.return_converters import ( CReturnConverter, return_converters, int_return_converter) +from libclinic.slots import SlotSpec, SLOTS from libclinic.parser import create_parser_namespace if TYPE_CHECKING: from libclinic.block_parser import Block from libclinic.app import Clinic +# Special methods which Argument Clinic cannot convert. The type slots +# which it supports are listed in libclinic.slots. unsupported_special_methods: set[str] = set(""" __abs__ @@ -76,7 +79,6 @@ __mod__ __mul__ __neg__ -__next__ __or__ __pos__ __pow__ @@ -265,6 +267,7 @@ class DSLParser: critical_section: bool target_critical_section: list[str] disable_fastcall: bool + slot: SlotSpec | None # Line of the file which is being parsed. line_number: int | None from_version_re = re.compile(r'([*/]) +\[from +(.+)\]') @@ -312,6 +315,7 @@ def reset(self) -> None: self.target_critical_section = [] self.disable_fastcall = False self.vectorcall: bool = False + self.slot = None self.permit_long_summary = False self.permit_long_docstring_body = False @@ -603,6 +607,9 @@ def parse_function_names(self, line: str) -> FunctionNames: fields = full_name.split(".") if fields[-1] == '__new__': fields.pop() + elif fields[-1] in SLOTS: + # list.__repr__ -> list_repr + fields[-1] = fields[-1].strip('_') c_basename = "_".join(fields) if not libclinic.is_legal_py_identifier(full_name): fail(f"Illegal function name: {full_name!r}") @@ -627,12 +634,23 @@ def normalize_function_kind(self, fullname: str) -> None: fail("'__new__' must be a class method!") if self.kind in ACCESSORS and not cls: fail("@getter and @setter must be methods") + slot = SLOTS.get(name) + if slot is not None: + if self.kind is not CALLABLE: + fail(f"{name!r} must be a normal method; got '{self.kind}'!") + if not cls: + fail(f"{name!r} must be a method!") + if self.coexist: + fail(f"@coexist cannot be applied to {name!r}") # Normalise self.kind. if name == '__new__': self.kind = METHOD_NEW elif name == '__init__': self.kind = METHOD_INIT + elif slot is not None: + self.kind = SLOT + self.slot = slot # Validate @vectorcall usage. if self.vectorcall: @@ -653,6 +671,9 @@ def resolve_return_converter( fail("@setter methods cannot define a return type") if self.kind is METHOD_INIT: fail("__init__ methods cannot define a return type") + if self.kind is SLOT: + fail(f"{full_name!r} cannot define a return type; " + "its type slot defines it") ast_input = f"def x() -> {forced_converter}: pass" try: module_node = ast.parse(ast_input) @@ -672,6 +693,9 @@ def resolve_return_converter( if self.kind in {METHOD_INIT} | SETTERS: return int_return_converter() + if self.kind is SLOT: + assert self.slot is not None + return return_converters[self.slot.return_converter]() return CReturnConverter() def parse_cloned_function(self, names: FunctionNames, existing: str) -> None: @@ -703,6 +727,8 @@ def parse_cloned_function(self, names: FunctionNames, existing: str) -> None: "docstring_line_number": None, "line_number": self.line_number, } + if existing_function.kind is SLOT or self.slot is not None: + fail("Type slot methods cannot be cloned") if not (existing_function.kind is self.kind and existing_function.coexist == self.coexist): # Allow __new__ or __init__ methods. @@ -777,6 +803,7 @@ def state_modulename_name(self, line: str) -> None: forced_text_signature=self.forced_text_signature, line_number=self.line_number, vectorcall=self.vectorcall, + slot=self.slot, ) self.add_function(func) @@ -893,6 +920,10 @@ def state_parameters_start(self, line: str) -> None: assert self.function is not None if self.function.kind is GETTER: fail("@getter methods cannot define parameters") + if self.function.kind is SLOT: + assert self.function.slot is not None + if not self.function.slot.takes_parameters: + fail(f"{self.function.name!r} methods cannot define parameters") self.parameter_continuation = '' return self.next(self.state_parameter, line) @@ -1498,6 +1529,12 @@ def format_docstring(self) -> str: assert self.function is not None f = self.function # For the following special cases, it does not make sense to render a docstring. + if f.kind is SLOT: + # A slot function has no docstring (there is no PyMethodDef). + if f.docstring: + fail(f"{f.name!r} methods cannot have a docstring", + line_number=f.docstring_line_number) + return f.docstring if f.kind in {METHOD_INIT, METHOD_NEW} | ACCESSORS and not f.docstring: return f.docstring @@ -1639,6 +1676,21 @@ def check_vectorcall_parameters(self, lineno: int) -> None: f"parse_arg(); parameter {p.name!r} does not", line_number=lineno) + def check_slot_parameters(self, func: Function) -> None: + """Check that the parameters of a slot method select a C slot.""" + assert func.slot is not None + params = list(func.parameters.values())[1:] + for p in params: + if not p.is_positional_only(): + fail(f"The parameters of {func.name!r} must be positional-only") + if p.default is not unspecified: + fail(f"The parameters of {func.name!r} cannot have a default value") + converters = [p.converter for p in params] + if func.slot.c_slot(converters) is None: + got = ', '.join(c.dsl_name() for c in converters) + fail(f"{func.name!r} must declare " + f"{func.slot.describe_signatures()}; got ({got})") + def do_post_block_processing_cleanup(self, lineno: int) -> None: """ Called when processing the block is done. @@ -1647,6 +1699,8 @@ def do_post_block_processing_cleanup(self, lineno: int) -> None: return func = self.function + if func.kind is SLOT: + self.check_slot_parameters(func) if func.kind in (SETTER, SETTER_AND_DELETER): # The new value is the only parameter of a setter. The setter # of a deletable attribute is also called with NULL to delete it, diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index e5d7469bb732aa..5270f4520180d4 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -11,6 +11,7 @@ from libclinic.converters import self_converter from libclinic.return_converters import CReturnConverter from libclinic.app import Clinic + from libclinic.slots import SlotSpec from libclinic import VersionTuple, unspecified @@ -94,6 +95,8 @@ class FunctionKind(enum.Enum): GETTER = enum.auto() SETTER = enum.auto() SETTER_AND_DELETER = enum.auto() + # A type slot ("tp slot"), see libclinic.slots. + SLOT = enum.auto() @functools.cached_property def new_or_init(self) -> bool: @@ -111,6 +114,7 @@ def __repr__(self) -> str: GETTER: Final = FunctionKind.GETTER SETTER: Final = FunctionKind.SETTER SETTER_AND_DELETER: Final = FunctionKind.SETTER_AND_DELETER +SLOT: Final = FunctionKind.SLOT # The kinds which implement the setter of an entry of PyGetSetDef. SETTERS: Final = frozenset({SETTER, SETTER_AND_DELETER}) @@ -156,6 +160,8 @@ class Function: # Line on which the docstring starts (`None` if there is no docstring). docstring_line_number: int | None = None vectorcall: bool = False + # The type slot which the function implements (kind is SLOT). + slot: SlotSpec | None = None def __post_init__(self) -> None: self.parent = self.cls or self.module @@ -224,7 +230,8 @@ def render_parameters(self) -> list[Parameter]: @property def methoddef_flags(self) -> str | None: - if self.kind.new_or_init: + if self.kind.new_or_init or self.kind is SLOT: + # Slot functions are not stored in a PyMethodDef. return None flags = [] match self.kind: diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index a3ab39376320de..73414e897e2615 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, SETTER_AND_DELETER, METHOD_INIT, SLOT, ACCESSORS, SETTERS) from libclinic.converter import CConverter from libclinic.converters import ( @@ -142,6 +142,14 @@ def declare_parser( static int {c_basename}({self_type}{self_name}, PyObject *arg, void *Py_UNUSED(context)) """) +PARSER_PROTOTYPE_SLOT: Final[str] = libclinic.normalize_snippet(""" + static {return_type} + {c_basename}({self_type}{self_name}) +""") +PARSER_PROTOTYPE_SLOT_ARGS: Final[str] = libclinic.normalize_snippet(""" + static {return_type} + {c_basename}({self_type}{self_name}, {parser_parameters}) +""") METH_O_PROTOTYPE: Final[str] = libclinic.normalize_snippet(""" static PyObject * {c_basename}({self_type}{self_name}, {parser_parameters}) @@ -393,8 +401,11 @@ def use_meth_o(self) -> bool: and not self.is_new_or_init()) def use_simple_return(self) -> bool: + if self.func.critical_section: + return False + # A slot function returns the result of the impl unchanged. return (self.func.return_converter.type == 'PyObject *' - and not self.func.critical_section) + or self.func.kind is SLOT) def use_pyobject_self(self) -> bool: return self.self_parameter_converter.use_pyobject_self(self.func) @@ -407,6 +418,19 @@ def select_prototypes(self) -> None: if self.is_new_or_init() and not self.func.docstring: pass + elif self.func.kind is SLOT: + # A slot function is not stored in a PyMethodDef and has no + # docstring (the DSL parser rejects one). It returns what its + # slot declares. + assert not self.func.docstring + assert self.func.slot is not None + self.methoddef_define = '' + return_type = self.func.slot.return_type + if not return_type.endswith('*'): + return_type += ' ' + self.return_value_declaration = ( + f"{return_type}{{parser_retval}} = " + f"{self.func.slot.error_value};") elif self.func.kind is GETTER: # Only an accessor compiled conditionally announces itself. self.methoddef_define = (GETTERDEF_PROTOTYPE_DEFINE @@ -482,6 +506,18 @@ def render_setter_value(self) -> str: libclinic.normalize_snippet("}}", indent=4), ]) + def parse_slot(self) -> None: + """Generate the slot function. + + It does not parse anything: it forwards its arguments to the impl + and, with @critical_section, holds the lock while doing so. + """ + if self.parameters: + self.parser_prototype = PARSER_PROTOTYPE_SLOT_ARGS + else: + self.parser_prototype = PARSER_PROTOTYPE_SLOT + self.finish_parser_body([]) + def parse_accessor(self) -> None: """Generate the code of a getter or a setter.""" parser_code: list[str] = [] @@ -1744,7 +1780,9 @@ def parse_args(self, clang: CLanguage) -> dict[str, str]: # previous call to parser_body. this is used for an awful hack. self.parser_body_fields: tuple[str, ...] = () - if self.func.kind in ACCESSORS: + if self.func.kind is SLOT: + self.parse_slot() + elif self.func.kind in ACCESSORS: self.parse_accessor() elif not self.parameters and not self.varpos and not self.var_keyword: self.parse_no_args() diff --git a/Tools/clinic/libclinic/slots.py b/Tools/clinic/libclinic/slots.py new file mode 100644 index 00000000000000..ee38b06c2ce4a8 --- /dev/null +++ b/Tools/clinic/libclinic/slots.py @@ -0,0 +1,59 @@ +"""Type slots ("tp slots") for which Argument Clinic generates the slot function. + +A slot function does not parse arguments: Argument Clinic generates a thin +wrapper with the C signature of the slot, which forwards its arguments to +the implementation function. The wrapper is the place where +``@critical_section`` acquires the lock. + +The parameters declared in the DSL only select the C signature; they must be +positional-only, without a default value. When a special method maps to +several slots (``__getitem__`` is ``sq_item`` or ``mp_subscript``), +the converters of its parameters determine which one it implements. +""" +from __future__ import annotations +import dataclasses as dc +from typing import Final + +from libclinic.converter import CConverter + + +@dc.dataclass(frozen=True, slots=True) +class SlotSpec: + # The special method name, e.g. '__next__'. + name: str + # The C return type of the slot function. + return_type: str + # The name of the return converter which declares `return_type`. + return_converter: str + # Maps the converter classes of the parameters (excluding self) to + # the C slot. + signatures: dict[tuple[type[CConverter], ...], str] + + @property + def error_value(self) -> str: + """The value returned by the slot function on error.""" + return 'NULL' if self.return_type.endswith('*') else '-1' + + @property + def takes_parameters(self) -> bool: + return any(self.signatures) + + def c_slot(self, converters: list[CConverter]) -> str | None: + """Return the C slot for the parameter converters, or None.""" + classes = tuple(type(c) for c in converters) + return self.signatures.get(classes) + + def describe_signatures(self) -> str: + return " or ".join( + f"({', '.join(c.dsl_name() for c in sig) or 'no parameters'})" + f" for {slot}" + for sig, slot in self.signatures.items()) + + +# Maps the name of a special method to its slot. A name listed here must +# not be in dsl_parser.unsupported_special_methods. +SLOTS: Final[dict[str, SlotSpec]] = { + '__next__': SlotSpec('__next__', 'PyObject *', 'object', { + (): 'tp_iternext', + }), +}