From 9b75ada6e4232ff03b716b1c5930d1a3ba3b16b8 Mon Sep 17 00:00:00 2001 From: "Erlend E. Aasland" Date: Sun, 13 Aug 2023 12:13:11 +0200 Subject: [PATCH] gh-107880: Teach Argument Clinic to clone __init__ and __new__ methods (#107885) --- Lib/test/test_clinic.py | 22 +++ ...-08-13-11-18-06.gh-issue-107880.gBVVQ7.rst | 2 + Modules/_testclinic.c | 34 ++++ Modules/clinic/_testclinic_depr_star.c.h | 168 +++++++++++++++++- Tools/clinic/clinic.py | 26 ++- 5 files changed, 244 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2023-08-13-11-18-06.gh-issue-107880.gBVVQ7.rst diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 55251b516d2..f067a26d1fb 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2973,6 +2973,17 @@ class ClinicFunctionalTest(unittest.TestCase): ac_tester.DeprStarNew(None) self.assertEqual(cm.filename, __file__) + def test_depr_star_new_cloned(self): + regex = re.escape( + "Passing positional arguments to _testclinic.DeprStarNew.cloned() " + "is deprecated. Parameter 'a' will become a keyword-only parameter " + "in Python 3.14." + ) + obj = ac_tester.DeprStarNew(a=None) + with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + obj.cloned(None) + self.assertEqual(cm.filename, __file__) + def test_depr_star_init(self): regex = re.escape( "Passing positional arguments to _testclinic.DeprStarInit() is " @@ -2983,6 +2994,17 @@ class ClinicFunctionalTest(unittest.TestCase): ac_tester.DeprStarInit(None) self.assertEqual(cm.filename, __file__) + def test_depr_star_init_cloned(self): + regex = re.escape( + "Passing positional arguments to _testclinic.DeprStarInit.cloned() " + "is deprecated. Parameter 'a' will become a keyword-only parameter " + "in Python 3.14." + ) + obj = ac_tester.DeprStarInit(a=None) + with self.assertWarnsRegex(DeprecationWarning, regex) as cm: + obj.cloned(None) + self.assertEqual(cm.filename, __file__) + def test_depr_star_pos0_len1(self): fn = ac_tester.depr_star_pos0_len1 fn(a=None) diff --git a/Misc/NEWS.d/next/Tools-Demos/2023-08-13-11-18-06.gh-issue-107880.gBVVQ7.rst b/Misc/NEWS.d/next/Tools-Demos/2023-08-13-11-18-06.gh-issue-107880.gBVVQ7.rst new file mode 100644 index 00000000000..fd9d6717f3a --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2023-08-13-11-18-06.gh-issue-107880.gBVVQ7.rst @@ -0,0 +1,2 @@ +Argument Clinic can now clone :meth:`!__init__` and :meth:`!__new__` +methods. diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 8fa3cc83d87..c33536234af 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -1230,12 +1230,29 @@ depr_star_new_impl(PyTypeObject *type, PyObject *a) return type->tp_alloc(type, 0); } +/*[clinic input] +_testclinic.DeprStarNew.cloned as depr_star_new_clone = _testclinic.DeprStarNew.__new__ +[clinic start generated code]*/ + +static PyObject * +depr_star_new_clone_impl(PyObject *type, PyObject *a) +/*[clinic end generated code: output=3b17bf885fa736bc input=ea659285d5dbec6c]*/ +{ + Py_RETURN_NONE; +} + +static struct PyMethodDef depr_star_new_methods[] = { + DEPR_STAR_NEW_CLONE_METHODDEF + {NULL, NULL} +}; + static PyTypeObject DeprStarNew = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "_testclinic.DeprStarNew", .tp_basicsize = sizeof(PyObject), .tp_new = depr_star_new, .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = depr_star_new_methods, }; @@ -1254,6 +1271,22 @@ depr_star_init_impl(PyObject *self, PyObject *a) return 0; } +/*[clinic input] +_testclinic.DeprStarInit.cloned as depr_star_init_clone = _testclinic.DeprStarInit.__init__ +[clinic start generated code]*/ + +static PyObject * +depr_star_init_clone_impl(PyObject *self, PyObject *a) +/*[clinic end generated code: output=ddfe8a1b5531e7cc input=561e103fe7f8e94f]*/ +{ + Py_RETURN_NONE; +} + +static struct PyMethodDef depr_star_init_methods[] = { + DEPR_STAR_INIT_CLONE_METHODDEF + {NULL, NULL} +}; + static PyTypeObject DeprStarInit = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "_testclinic.DeprStarInit", @@ -1261,6 +1294,7 @@ static PyTypeObject DeprStarInit = { .tp_new = PyType_GenericNew, .tp_init = depr_star_init, .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = depr_star_init_methods, }; diff --git a/Modules/clinic/_testclinic_depr_star.c.h b/Modules/clinic/_testclinic_depr_star.c.h index 0c2fa088268..1aa42dd4059 100644 --- a/Modules/clinic/_testclinic_depr_star.c.h +++ b/Modules/clinic/_testclinic_depr_star.c.h @@ -92,6 +92,89 @@ exit: return return_value; } +PyDoc_STRVAR(depr_star_new_clone__doc__, +"cloned($self, /, a)\n" +"--\n" +"\n" +"Note: Passing positional arguments to _testclinic.DeprStarNew.cloned()\n" +"is deprecated. Parameter \'a\' will become a keyword-only parameter in\n" +"Python 3.14.\n" +""); + +#define DEPR_STAR_NEW_CLONE_METHODDEF \ + {"cloned", _PyCFunction_CAST(depr_star_new_clone), METH_FASTCALL|METH_KEYWORDS, depr_star_new_clone__doc__}, + +static PyObject * +depr_star_new_clone_impl(PyObject *type, PyObject *a); + +static PyObject * +depr_star_new_clone(PyObject *type, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(a), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"a", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "cloned", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *a; + + // Emit compiler warnings when we get to Python 3.14. + #if PY_VERSION_HEX >= 0x030e00C0 + # error \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarNew.cloned' to be keyword-only." + #elif PY_VERSION_HEX >= 0x030e00A0 + # ifdef _MSC_VER + # pragma message ( \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarNew.cloned' to be keyword-only.") + # else + # warning \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarNew.cloned' to be keyword-only." + # endif + #endif + if (nargs == 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Passing positional arguments to _testclinic.DeprStarNew.cloned()" + " is deprecated. Parameter 'a' will become a keyword-only " + "parameter in Python 3.14.", 1)) + { + goto exit; + } + } + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf); + if (!args) { + goto exit; + } + a = args[0]; + return_value = depr_star_new_clone_impl(type, a); + +exit: + return return_value; +} + PyDoc_STRVAR(depr_star_init__doc__, "DeprStarInit(a)\n" "--\n" @@ -176,6 +259,89 @@ exit: return return_value; } +PyDoc_STRVAR(depr_star_init_clone__doc__, +"cloned($self, /, a)\n" +"--\n" +"\n" +"Note: Passing positional arguments to\n" +"_testclinic.DeprStarInit.cloned() is deprecated. Parameter \'a\' will\n" +"become a keyword-only parameter in Python 3.14.\n" +""); + +#define DEPR_STAR_INIT_CLONE_METHODDEF \ + {"cloned", _PyCFunction_CAST(depr_star_init_clone), METH_FASTCALL|METH_KEYWORDS, depr_star_init_clone__doc__}, + +static PyObject * +depr_star_init_clone_impl(PyObject *self, PyObject *a); + +static PyObject * +depr_star_init_clone(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(a), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"a", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "cloned", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + PyObject *a; + + // Emit compiler warnings when we get to Python 3.14. + #if PY_VERSION_HEX >= 0x030e00C0 + # error \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarInit.cloned' to be keyword-only." + #elif PY_VERSION_HEX >= 0x030e00A0 + # ifdef _MSC_VER + # pragma message ( \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarInit.cloned' to be keyword-only.") + # else + # warning \ + "In _testclinic.c, update parameter(s) 'a' in the clinic input of" \ + " '_testclinic.DeprStarInit.cloned' to be keyword-only." + # endif + #endif + if (nargs == 1) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Passing positional arguments to " + "_testclinic.DeprStarInit.cloned() is deprecated. Parameter 'a' " + "will become a keyword-only parameter in Python 3.14.", 1)) + { + goto exit; + } + } + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, argsbuf); + if (!args) { + goto exit; + } + a = args[0]; + return_value = depr_star_init_clone_impl(self, a); + +exit: + return return_value; +} + PyDoc_STRVAR(depr_star_pos0_len1__doc__, "depr_star_pos0_len1($module, /, a)\n" "--\n" @@ -971,4 +1137,4 @@ depr_star_pos2_len2_with_kwd(PyObject *module, PyObject *const *args, Py_ssize_t exit: return return_value; } -/*[clinic end generated code: output=18ab056f6cc06d7e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7a16fee4d6742d54 input=a9049054013a1b77]*/ diff --git a/Tools/clinic/clinic.py b/Tools/clinic/clinic.py index 2d23f9d1287..1e0303c7708 100755 --- a/Tools/clinic/clinic.py +++ b/Tools/clinic/clinic.py @@ -4888,13 +4888,25 @@ class DSLParser: function_name = fields.pop() module, cls = self.clinic._module_and_class(fields) - if not (existing_function.kind is self.kind and existing_function.coexist == self.coexist): - fail("'kind' of function and cloned function don't match! " - "(@classmethod/@staticmethod/@coexist)") - function = existing_function.copy( - name=function_name, full_name=full_name, module=module, - cls=cls, c_basename=c_basename, docstring='' - ) + overrides: dict[str, Any] = { + "name": function_name, + "full_name": full_name, + "module": module, + "cls": cls, + "c_basename": c_basename, + "docstring": "", + } + if not (existing_function.kind is self.kind and + existing_function.coexist == self.coexist): + # Allow __new__ or __init__ methods. + if existing_function.kind.new_or_init: + overrides["kind"] = self.kind + # Future enhancement: allow custom return converters + overrides["return_converter"] = CReturnConverter() + else: + fail("'kind' of function and cloned function don't match! " + "(@classmethod/@staticmethod/@coexist)") + function = existing_function.copy(**overrides) self.function = function self.block.signatures.append(function) (cls or module).functions.append(function)