gh-95065: Add Argument Clinic support for deprecating positional use of parameters (#95151)

It is now possible to deprecate passing parameters positionally with
Argument Clinic, using the new '* [from X.Y]' syntax.
(To be read as "keyword-only from Python version X.Y")

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Erlend E. Aasland 2023-08-07 13:28:08 +02:00 committed by GitHub
parent 3c8e8f3cee
commit 33cb0b06ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 1153 additions and 14 deletions

View file

@ -1898,3 +1898,91 @@ blocks embedded in Python files look slightly different. They look like this:
#[python start generated code]*/
def foo(): pass
#/*[python checksum:...]*/
.. _clinic-howto-deprecate-positional:
How to deprecate passing parameters positionally
------------------------------------------------
Argument Clinic provides syntax that makes it possible to generate code that
deprecates passing :term:`arguments <argument>` positionally.
For example, say we've got a module-level function :py:func:`!foo.myfunc`
that has three :term:`parameters <parameter>`:
positional-or-keyword parameters *a* and *b*, and a keyword-only parameter *c*::
/*[clinic input]
module foo
myfunc
a: int
b: int
*
c: int
[clinic start generated output]*/
We now want to make the *b* parameter keyword-only;
however, we'll have to wait two releases before making this change,
as mandated by Python's backwards-compatibility policy (see :pep:`387`).
For this example, imagine we're in the development phase for Python 3.12:
that means we'll be allowed to introduce deprecation warnings in Python 3.12
whenever the *b* parameter is passed positionally,
and we'll be allowed to make it keyword-only in Python 3.14 at the earliest.
We can use Argument Clinic to emit the desired deprecation warnings
using the ``* [from ...]``` syntax,
by adding the line ``* [from 3.14]`` right above the *b* parameter::
/*[clinic input]
module foo
myfunc
a: int
* [from 3.14]
b: int
*
c: int
[clinic start generated output]*/
Next, regenerate Argument Clinic code (``make clinic``),
and add unit tests for the new behaviour.
The generated code will now emit a :exc:`DeprecationWarning`
when an :term:`argument` for the :term:`parameter` *b* is passed positionally.
C preprocessor directives are also generated for emitting
compiler warnings if the ``* [from ...]`` line has not been removed
from the Argument Clinic input when the deprecation period is over,
which means when the alpha phase of the specified Python version kicks in.
Let's return to our example and skip ahead two years:
Python 3.14 development has now entered the alpha phase,
but we forgot all about updating the Argument Clinic code
for :py:func:`!myfunc`!
Luckily for us, compiler warnings are now generated:
.. code-block:: none
In file included from Modules/foomodule.c:139:
Modules/clinic/foomodule.c.h:83:8: warning: Update 'b' in 'myfunc' in 'foomodule.c' to be keyword-only. [-W#warnings]
# warning "Update 'b' in 'myfunc' in 'foomodule.c' to be keyword-only."
^
We now close the deprecation phase by making *b* keyword-only;
replace the ``* [from ...]``` line above *b*
with the ``*`` from the line above *c*::
/*[clinic input]
module foo
myfunc
a: int
*
b: int
c: int
[clinic start generated output]*/
Finally, run ``make clinic`` to regenerate the Argument Clinic code,
and update your unit tests to reflect the new behaviour.
.. note::
If you forget to update your input block during the alpha and beta phases,
the compiler warning will turn into a compiler error when the
release candidate phase begins.

View file

@ -5380,6 +5380,7 @@ static PyObject *
fn_with_default_binop_expr_impl(PyObject *module, PyObject *arg)
/*[clinic end generated code: output=018672772e4092ff input=1b55c8ae68d89453]*/
/*[python input]
class Custom_converter(CConverter):
type = "str"
@ -5464,3 +5465,812 @@ exit:
static PyObject *
docstr_fallback_to_converter_default_impl(PyObject *module, str a)
/*[clinic end generated code: output=ae24a9c6f60ee8a6 input=0cbe6a4d24bc2274]*/
/*[clinic input]
test_deprecate_positional_pos1_len1_optional
a: object
* [from 3.14]
b: object = None
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos1_len1_optional__doc__,
"test_deprecate_positional_pos1_len1_optional($module, /, a, b=None)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS1_LEN1_OPTIONAL_METHODDEF \
{"test_deprecate_positional_pos1_len1_optional", _PyCFunction_CAST(test_deprecate_positional_pos1_len1_optional), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos1_len1_optional__doc__},
static PyObject *
test_deprecate_positional_pos1_len1_optional_impl(PyObject *module,
PyObject *a, PyObject *b);
static PyObject *
test_deprecate_positional_pos1_len1_optional(PyObject *module, 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 2
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), &_Py_ID(b), },
};
#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", "b", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos1_len1_optional",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
PyObject *a;
PyObject *b = Py_None;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1_optional' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1_optional' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1_optional' to be keyword-only."
# endif
#endif
if (nargs == 2) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing 2 positional arguments to test_deprecate_positional_pos1_len1_optional() is deprecated. Parameter 'b' will become a keyword-only parameter in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 1, 2, 0, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
if (!noptargs) {
goto skip_optional_pos;
}
b = args[1];
skip_optional_pos:
return_value = test_deprecate_positional_pos1_len1_optional_impl(module, a, b);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos1_len1_optional_impl(PyObject *module,
PyObject *a, PyObject *b)
/*[clinic end generated code: output=20bdea6a2960ddf3 input=89099f3dacd757da]*/
/*[clinic input]
test_deprecate_positional_pos1_len1
a: object
* [from 3.14]
b: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos1_len1__doc__,
"test_deprecate_positional_pos1_len1($module, /, a, b)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS1_LEN1_METHODDEF \
{"test_deprecate_positional_pos1_len1", _PyCFunction_CAST(test_deprecate_positional_pos1_len1), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos1_len1__doc__},
static PyObject *
test_deprecate_positional_pos1_len1_impl(PyObject *module, PyObject *a,
PyObject *b);
static PyObject *
test_deprecate_positional_pos1_len1(PyObject *module, 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 2
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), &_Py_ID(b), },
};
#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", "b", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos1_len1",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
PyObject *a;
PyObject *b;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'b' in the clinic input of 'test_deprecate_positional_pos1_len1' to be keyword-only."
# endif
#endif
if (nargs == 2) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing 2 positional arguments to test_deprecate_positional_pos1_len1() is deprecated. Parameter 'b' will become a keyword-only parameter in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
return_value = test_deprecate_positional_pos1_len1_impl(module, a, b);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos1_len1_impl(PyObject *module, PyObject *a,
PyObject *b)
/*[clinic end generated code: output=22c70f8b36085758 input=1702bbab1e9b3b99]*/
/*[clinic input]
test_deprecate_positional_pos1_len2_with_kwd
a: object
* [from 3.14]
b: object
c: object
*
d: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos1_len2_with_kwd__doc__,
"test_deprecate_positional_pos1_len2_with_kwd($module, /, a, b, c, *, d)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS1_LEN2_WITH_KWD_METHODDEF \
{"test_deprecate_positional_pos1_len2_with_kwd", _PyCFunction_CAST(test_deprecate_positional_pos1_len2_with_kwd), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos1_len2_with_kwd__doc__},
static PyObject *
test_deprecate_positional_pos1_len2_with_kwd_impl(PyObject *module,
PyObject *a, PyObject *b,
PyObject *c, PyObject *d);
static PyObject *
test_deprecate_positional_pos1_len2_with_kwd(PyObject *module, 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 4
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), &_Py_ID(b), &_Py_ID(c), &_Py_ID(d), },
};
#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", "b", "c", "d", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos1_len2_with_kwd",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[4];
PyObject *a;
PyObject *b;
PyObject *c;
PyObject *d;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos1_len2_with_kwd' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos1_len2_with_kwd' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos1_len2_with_kwd' to be keyword-only."
# endif
#endif
if (nargs > 1 && nargs <= 3) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing more than 1 positional argument to test_deprecate_positional_pos1_len2_with_kwd() is deprecated. Parameters 'b' and 'c' will become keyword-only parameters in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 1, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
c = args[2];
d = args[3];
return_value = test_deprecate_positional_pos1_len2_with_kwd_impl(module, a, b, c, d);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos1_len2_with_kwd_impl(PyObject *module,
PyObject *a, PyObject *b,
PyObject *c, PyObject *d)
/*[clinic end generated code: output=79c5f04220a1f3aa input=28cdb885f6c34eab]*/
/*[clinic input]
test_deprecate_positional_pos0_len1
* [from 3.14]
a: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos0_len1__doc__,
"test_deprecate_positional_pos0_len1($module, /, a)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS0_LEN1_METHODDEF \
{"test_deprecate_positional_pos0_len1", _PyCFunction_CAST(test_deprecate_positional_pos0_len1), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos0_len1__doc__},
static PyObject *
test_deprecate_positional_pos0_len1_impl(PyObject *module, PyObject *a);
static PyObject *
test_deprecate_positional_pos0_len1(PyObject *module, 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 = "test_deprecate_positional_pos0_len1",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[1];
PyObject *a;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'a' in the clinic input of 'test_deprecate_positional_pos0_len1' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'a' in the clinic input of 'test_deprecate_positional_pos0_len1' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'a' in the clinic input of 'test_deprecate_positional_pos0_len1' to be keyword-only."
# endif
#endif
if (nargs == 1) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing positional arguments to test_deprecate_positional_pos0_len1() 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 = test_deprecate_positional_pos0_len1_impl(module, a);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos0_len1_impl(PyObject *module, PyObject *a)
/*[clinic end generated code: output=1b7f23b9ffca431b input=678206db25c0652c]*/
/*[clinic input]
test_deprecate_positional_pos0_len2
* [from 3.14]
a: object
b: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos0_len2__doc__,
"test_deprecate_positional_pos0_len2($module, /, a, b)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS0_LEN2_METHODDEF \
{"test_deprecate_positional_pos0_len2", _PyCFunction_CAST(test_deprecate_positional_pos0_len2), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos0_len2__doc__},
static PyObject *
test_deprecate_positional_pos0_len2_impl(PyObject *module, PyObject *a,
PyObject *b);
static PyObject *
test_deprecate_positional_pos0_len2(PyObject *module, 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 2
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), &_Py_ID(b), },
};
#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", "b", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos0_len2",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[2];
PyObject *a;
PyObject *b;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'a' and 'b' in the clinic input of 'test_deprecate_positional_pos0_len2' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'a' and 'b' in the clinic input of 'test_deprecate_positional_pos0_len2' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'a' and 'b' in the clinic input of 'test_deprecate_positional_pos0_len2' to be keyword-only."
# endif
#endif
if (nargs > 0 && nargs <= 2) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing positional arguments to test_deprecate_positional_pos0_len2() is deprecated. Parameters 'a' and 'b' will become keyword-only parameters in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
return_value = test_deprecate_positional_pos0_len2_impl(module, a, b);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos0_len2_impl(PyObject *module, PyObject *a,
PyObject *b)
/*[clinic end generated code: output=31b494f2dcc016af input=fae0d0b1d480c939]*/
/*[clinic input]
test_deprecate_positional_pos0_len3_with_kwdonly
* [from 3.14]
a: object
b: object
c: object
*
e: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos0_len3_with_kwdonly__doc__,
"test_deprecate_positional_pos0_len3_with_kwdonly($module, /, a, b, c,\n"
" *, e)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS0_LEN3_WITH_KWDONLY_METHODDEF \
{"test_deprecate_positional_pos0_len3_with_kwdonly", _PyCFunction_CAST(test_deprecate_positional_pos0_len3_with_kwdonly), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos0_len3_with_kwdonly__doc__},
static PyObject *
test_deprecate_positional_pos0_len3_with_kwdonly_impl(PyObject *module,
PyObject *a,
PyObject *b,
PyObject *c,
PyObject *e);
static PyObject *
test_deprecate_positional_pos0_len3_with_kwdonly(PyObject *module, 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 4
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), &_Py_ID(b), &_Py_ID(c), &_Py_ID(e), },
};
#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", "b", "c", "e", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos0_len3_with_kwdonly",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[4];
PyObject *a;
PyObject *b;
PyObject *c;
PyObject *e;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'a', 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos0_len3_with_kwdonly' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'a', 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos0_len3_with_kwdonly' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'a', 'b' and 'c' in the clinic input of 'test_deprecate_positional_pos0_len3_with_kwdonly' to be keyword-only."
# endif
#endif
if (nargs > 0 && nargs <= 3) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing positional arguments to test_deprecate_positional_pos0_len3_with_kwdonly() is deprecated. Parameters 'a', 'b' and 'c' will become keyword-only parameters in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 1, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
c = args[2];
e = args[3];
return_value = test_deprecate_positional_pos0_len3_with_kwdonly_impl(module, a, b, c, e);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos0_len3_with_kwdonly_impl(PyObject *module,
PyObject *a,
PyObject *b,
PyObject *c,
PyObject *e)
/*[clinic end generated code: output=96978e786acfbc7b input=1b0121770c0c52e0]*/
/*[clinic input]
test_deprecate_positional_pos2_len1
a: object
b: object
* [from 3.14]
c: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos2_len1__doc__,
"test_deprecate_positional_pos2_len1($module, /, a, b, c)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS2_LEN1_METHODDEF \
{"test_deprecate_positional_pos2_len1", _PyCFunction_CAST(test_deprecate_positional_pos2_len1), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos2_len1__doc__},
static PyObject *
test_deprecate_positional_pos2_len1_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *c);
static PyObject *
test_deprecate_positional_pos2_len1(PyObject *module, 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 3
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), &_Py_ID(b), &_Py_ID(c), },
};
#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", "b", "c", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos2_len1",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[3];
PyObject *a;
PyObject *b;
PyObject *c;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'c' in the clinic input of 'test_deprecate_positional_pos2_len1' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'c' in the clinic input of 'test_deprecate_positional_pos2_len1' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'c' in the clinic input of 'test_deprecate_positional_pos2_len1' to be keyword-only."
# endif
#endif
if (nargs == 3) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing 3 positional arguments to test_deprecate_positional_pos2_len1() is deprecated. Parameter 'c' will become a keyword-only parameter in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 3, 3, 0, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
c = args[2];
return_value = test_deprecate_positional_pos2_len1_impl(module, a, b, c);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos2_len1_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *c)
/*[clinic end generated code: output=ceadd05f11f7f491 input=e1d129689e69ec7c]*/
/*[clinic input]
test_deprecate_positional_pos2_len2
a: object
b: object
* [from 3.14]
c: object
d: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos2_len2__doc__,
"test_deprecate_positional_pos2_len2($module, /, a, b, c, d)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS2_LEN2_METHODDEF \
{"test_deprecate_positional_pos2_len2", _PyCFunction_CAST(test_deprecate_positional_pos2_len2), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos2_len2__doc__},
static PyObject *
test_deprecate_positional_pos2_len2_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *c,
PyObject *d);
static PyObject *
test_deprecate_positional_pos2_len2(PyObject *module, 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 4
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), &_Py_ID(b), &_Py_ID(c), &_Py_ID(d), },
};
#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", "b", "c", "d", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos2_len2",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[4];
PyObject *a;
PyObject *b;
PyObject *c;
PyObject *d;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len2' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len2' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len2' to be keyword-only."
# endif
#endif
if (nargs > 2 && nargs <= 4) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing more than 2 positional arguments to test_deprecate_positional_pos2_len2() is deprecated. Parameters 'c' and 'd' will become keyword-only parameters in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 4, 4, 0, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
c = args[2];
d = args[3];
return_value = test_deprecate_positional_pos2_len2_impl(module, a, b, c, d);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos2_len2_impl(PyObject *module, PyObject *a,
PyObject *b, PyObject *c,
PyObject *d)
/*[clinic end generated code: output=5693682e3fa1188b input=0d53533463a12792]*/
/*[clinic input]
test_deprecate_positional_pos2_len3_with_kwdonly
a: object
b: object
* [from 3.14]
c: object
d: object
*
e: object
[clinic start generated code]*/
PyDoc_STRVAR(test_deprecate_positional_pos2_len3_with_kwdonly__doc__,
"test_deprecate_positional_pos2_len3_with_kwdonly($module, /, a, b, c,\n"
" d, *, e)\n"
"--\n"
"\n");
#define TEST_DEPRECATE_POSITIONAL_POS2_LEN3_WITH_KWDONLY_METHODDEF \
{"test_deprecate_positional_pos2_len3_with_kwdonly", _PyCFunction_CAST(test_deprecate_positional_pos2_len3_with_kwdonly), METH_FASTCALL|METH_KEYWORDS, test_deprecate_positional_pos2_len3_with_kwdonly__doc__},
static PyObject *
test_deprecate_positional_pos2_len3_with_kwdonly_impl(PyObject *module,
PyObject *a,
PyObject *b,
PyObject *c,
PyObject *d,
PyObject *e);
static PyObject *
test_deprecate_positional_pos2_len3_with_kwdonly(PyObject *module, 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 5
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), &_Py_ID(b), &_Py_ID(c), &_Py_ID(d), &_Py_ID(e), },
};
#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", "b", "c", "d", "e", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "test_deprecate_positional_pos2_len3_with_kwdonly",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
PyObject *argsbuf[5];
PyObject *a;
PyObject *b;
PyObject *c;
PyObject *d;
PyObject *e;
#if PY_VERSION_HEX >= 0x030e00C0
# error "In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len3_with_kwdonly' to be keyword-only."
#elif PY_VERSION_HEX >= 0x030e00A0
# ifdef _MSC_VER
# pragma message ("In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len3_with_kwdonly' to be keyword-only.")
# else
# warning "In clinic.test.c, update parameter(s) 'c' and 'd' in the clinic input of 'test_deprecate_positional_pos2_len3_with_kwdonly' to be keyword-only."
# endif
#endif
if (nargs > 2 && nargs <= 4) {
if (PyErr_WarnEx(PyExc_DeprecationWarning, "Passing more than 2 positional arguments to test_deprecate_positional_pos2_len3_with_kwdonly() is deprecated. Parameters 'c' and 'd' will become keyword-only parameters in Python 3.14.", 1)) {
goto exit;
}
}
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 4, 4, 1, argsbuf);
if (!args) {
goto exit;
}
a = args[0];
b = args[1];
c = args[2];
d = args[3];
e = args[4];
return_value = test_deprecate_positional_pos2_len3_with_kwdonly_impl(module, a, b, c, d, e);
exit:
return return_value;
}
static PyObject *
test_deprecate_positional_pos2_len3_with_kwdonly_impl(PyObject *module,
PyObject *a,
PyObject *b,
PyObject *c,
PyObject *d,
PyObject *e)
/*[clinic end generated code: output=00d436de747a00f3 input=154fd450448d8935]*/

View file

@ -1478,11 +1478,105 @@ class ClinicParserTest(TestCase):
"module foo\nfoo.bar\n this: int\n *",
"module foo\nfoo.bar\n this: int\n *\nDocstring.",
)
err = "Function 'bar' specifies '*' without any parameters afterwards."
err = "Function 'foo.bar' specifies '*' without any parameters afterwards."
for block in dataset:
with self.subTest(block=block):
self.expect_failure(block, err)
def test_parameters_required_after_depr_star(self):
dataset = (
"module foo\nfoo.bar\n * [from 3.14]",
"module foo\nfoo.bar\n * [from 3.14]\nDocstring here.",
"module foo\nfoo.bar\n this: int\n * [from 3.14]",
"module foo\nfoo.bar\n this: int\n * [from 3.14]\nDocstring.",
)
err = "Function 'foo.bar' specifies '* [from 3.14]' without any parameters afterwards."
for block in dataset:
with self.subTest(block=block):
self.expect_failure(block, err)
def test_depr_star_invalid_format_1(self):
block = """
module foo
foo.bar
this: int
* [from 3]
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"where 'major' and 'minor' are integers; got '3'"
)
self.expect_failure(block, err, lineno=3)
def test_depr_star_invalid_format_2(self):
block = """
module foo
foo.bar
this: int
* [from a.b]
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"where 'major' and 'minor' are integers; got 'a.b'"
)
self.expect_failure(block, err, lineno=3)
def test_depr_star_invalid_format_3(self):
block = """
module foo
foo.bar
this: int
* [from 1.2.3]
Docstring.
"""
err = (
"Function 'foo.bar': expected format '* [from major.minor]' "
"where 'major' and 'minor' are integers; got '1.2.3'"
)
self.expect_failure(block, err, lineno=3)
def test_parameters_required_after_depr_star(self):
block = """
module foo
foo.bar
this: int
* [from 3.14]
Docstring.
"""
err = (
"Function 'foo.bar' specifies '* [from ...]' without "
"any parameters afterwards"
)
self.expect_failure(block, err, lineno=4)
def test_depr_star_must_come_before_star(self):
block = """
module foo
foo.bar
this: int
*
* [from 3.14]
Docstring.
"""
err = "Function 'foo.bar': '* [from ...]' must come before '*'"
self.expect_failure(block, err, lineno=4)
def test_depr_star_duplicate(self):
block = """
module foo
foo.bar
a: int
* [from 3.14]
b: int
* [from 3.14]
c: int
Docstring.
"""
err = "Function 'foo.bar' uses '[from ...]' more than once"
self.expect_failure(block, err, lineno=5)
def test_single_slash(self):
block = """
module foo

View file

@ -0,0 +1,6 @@
It is now possible to deprecate passing parameters positionally with
Argument Clinic, using the new ``* [from X.Y]`` syntax.
(To be read as *"keyword-only from Python version X.Y"*.)
See :ref:`clinic-howto-deprecate-positional` for more information.
Patch by Erlend E. Aasland with help from Alex Waygood,
Nikita Sobolev, and Serhiy Storchaka.

View file

@ -347,6 +347,13 @@ def suffix_all_lines(s: str, suffix: str) -> str:
return ''.join(final)
def pprint_words(items: list[str]) -> str:
if len(items) <= 2:
return " and ".join(items)
else:
return ", ".join(items[:-1]) + " and " + items[-1]
def version_splitter(s: str) -> tuple[int, ...]:
"""Splits a version string into a tuple of integers.
@ -828,6 +835,22 @@ class CLanguage(Language):
#define {methoddef_name}
#endif /* !defined({methoddef_name}) */
""")
DEPRECATED_POSITIONAL_PROTOTYPE: Final[str] = r"""
#if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
# error "{cpp_message}"
#elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
# ifdef _MSC_VER
# pragma message ("{cpp_message}")
# else
# warning "{cpp_message}"
# endif
#endif
if ({condition}) {{{{
if (PyErr_WarnEx(PyExc_DeprecationWarning, "{depr_message}", 1)) {{{{
goto exit;
}}}}
}}}}
"""
def __init__(self, filename: str) -> None:
super().__init__(filename)
@ -850,6 +873,64 @@ class CLanguage(Language):
function = o
return self.render_function(clinic, function)
def deprecate_positional_use(
self,
func: Function,
params: dict[int, Parameter],
) -> str:
assert len(params) > 0
names = [repr(p.name) for p in params.values()]
first_pos, first_param = next(iter(params.items()))
last_pos, last_param = next(reversed(params.items()))
# Pretty-print list of names.
pstr = pprint_words(names)
# For now, assume there's only one deprecation level.
assert first_param.deprecated_positional == last_param.deprecated_positional
thenceforth = first_param.deprecated_positional
assert thenceforth is not None
# Format the preprocessor warning and error messages.
assert isinstance(self.cpp.filename, str)
source = os.path.basename(self.cpp.filename)
major, minor = thenceforth
cpp_message = (
f"In {source}, update parameter(s) {pstr} in the clinic "
f"input of {func.full_name!r} to be keyword-only."
)
# Format the deprecation message.
if first_pos == 0:
preamble = "Passing positional arguments to "
if len(params) == 1:
condition = f"nargs == {first_pos+1}"
if first_pos:
preamble = f"Passing {first_pos+1} positional arguments to "
depr_message = preamble + (
f"{func.full_name}() is deprecated. Parameter {pstr} will "
f"become a keyword-only parameter in Python {major}.{minor}."
)
else:
condition = f"nargs > {first_pos} && nargs <= {last_pos+1}"
if first_pos:
preamble = (
f"Passing more than {first_pos} positional "
f"argument{'s' if first_pos != 1 else ''} to "
)
depr_message = preamble + (
f"{func.full_name}() is deprecated. Parameters {pstr} will "
f"become keyword-only parameters in Python {major}.{minor}."
)
# Format and return the code block.
code = self.DEPRECATED_POSITIONAL_PROTOTYPE.format(
condition=condition,
major=major,
minor=minor,
cpp_message=cpp_message,
depr_message=depr_message,
)
return normalize_snippet(code, indent=4)
def docstring_for_c_string(
self,
f: Function
@ -1199,6 +1280,7 @@ class CLanguage(Language):
flags = 'METH_METHOD|' + flags
parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS
deprecated_positionals: dict[int, Parameter] = {}
add_label: str | None = None
for i, p in enumerate(parameters):
if isinstance(p.converter, defining_class_converter):
@ -1213,6 +1295,8 @@ class CLanguage(Language):
parser_code.append("%s:" % add_label)
add_label = None
if not p.is_optional():
if p.deprecated_positional:
deprecated_positionals[i] = p
parser_code.append(normalize_snippet(parsearg, indent=4))
elif i < pos_only:
add_label = 'skip_optional_posonly'
@ -1242,6 +1326,8 @@ class CLanguage(Language):
goto %s;
}}
""" % add_label, indent=4))
if p.deprecated_positional:
deprecated_positionals[i] = p
if i + 1 == len(parameters):
parser_code.append(normalize_snippet(parsearg, indent=4))
else:
@ -1257,6 +1343,12 @@ class CLanguage(Language):
}}
""" % add_label, indent=4))
if deprecated_positionals:
code = self.deprecate_positional_use(f, deprecated_positionals)
assert parser_code is not None
# Insert the deprecation code before parameter parsing.
parser_code.insert(0, code)
if parser_code is not None:
if add_label:
parser_code.append("%s:" % add_label)
@ -2592,6 +2684,9 @@ class Function:
return f
VersionTuple = tuple[int, int]
@dc.dataclass(repr=False, slots=True)
class Parameter:
"""
@ -2606,6 +2701,8 @@ class Parameter:
annotation: object = inspect.Parameter.empty
docstring: str = ''
group: int = 0
# (`None` signifies that there is no deprecation)
deprecated_positional: VersionTuple | None = None
right_bracket_count: int = dc.field(init=False, default=0)
def __repr__(self) -> str:
@ -4430,6 +4527,7 @@ class DSLParser:
state: StateKeeper
keyword_only: bool
positional_only: bool
deprecated_positional: VersionTuple | None
group: int
parameter_state: ParamState
indent: IndentStack
@ -4437,6 +4535,11 @@ class DSLParser:
coexist: bool
parameter_continuation: str
preserve_output: bool
star_from_version_re = create_regex(
before="* [from ",
after="]",
word=False,
)
def __init__(self, clinic: Clinic) -> None:
self.clinic = clinic
@ -4460,6 +4563,7 @@ class DSLParser:
self.state = self.state_dsl_start
self.keyword_only = False
self.positional_only = False
self.deprecated_positional = None
self.group = 0
self.parameter_state: ParamState = ParamState.START
self.indent = IndentStack()
@ -4622,7 +4726,7 @@ class DSLParser:
exc.lineno = line_number
raise
self.do_post_block_processing_cleanup()
self.do_post_block_processing_cleanup(line_number)
block.output.extend(self.clinic.language.render(self.clinic, block.signatures))
if self.preserve_output:
@ -4908,8 +5012,14 @@ class DSLParser:
self.parameter_continuation = line[:-1]
return
line = line.lstrip()
match = self.star_from_version_re.match(line)
if match:
self.parse_deprecated_positional(match.group(1))
return
func = self.function
match line.lstrip():
match line:
case '*':
self.parse_star(func)
case '[':
@ -5182,7 +5292,9 @@ class DSLParser:
"after 'self'.")
p = Parameter(parameter_name, kind, function=self.function, converter=converter, default=value, group=self.group)
p = Parameter(parameter_name, kind, function=self.function,
converter=converter, default=value, group=self.group,
deprecated_positional=self.deprecated_positional)
names = [k.name for k in self.function.parameters.values()]
if parameter_name in names[1:]:
@ -5215,10 +5327,28 @@ class DSLParser:
"Annotations must be either a name, a function call, or a string."
)
def parse_deprecated_positional(self, thenceforth: str) -> None:
assert isinstance(self.function, Function)
fname = self.function.full_name
if self.keyword_only:
fail(f"Function {fname!r}: '* [from ...]' must come before '*'")
if self.deprecated_positional:
fail(f"Function {fname!r} uses '[from ...]' more than once.")
try:
major, minor = thenceforth.split(".")
self.deprecated_positional = int(major), int(minor)
except ValueError:
fail(
f"Function {fname!r}: expected format '* [from major.minor]' "
f"where 'major' and 'minor' are integers; got {thenceforth!r}"
)
def parse_star(self, function: Function) -> None:
"""Parse keyword-only parameter marker '*'."""
if self.keyword_only:
fail(f"Function {function.name!r} uses '*' more than once.")
self.deprecated_positional = None
self.keyword_only = True
def parse_opening_square_bracket(self, function: Function) -> None:
@ -5586,23 +5716,34 @@ class DSLParser:
return docstring
def do_post_block_processing_cleanup(self) -> None:
def do_post_block_processing_cleanup(self, lineno: int) -> None:
"""
Called when processing the block is done.
"""
if not self.function:
return
if self.keyword_only:
values = self.function.parameters.values()
if not values:
no_parameter_after_star = True
def check_remaining(
symbol: str,
condition: Callable[[Parameter], bool]
) -> None:
assert isinstance(self.function, Function)
if values := self.function.parameters.values():
last_param = next(reversed(values))
no_param_after_symbol = condition(last_param)
else:
last_parameter = next(reversed(list(values)))
no_parameter_after_star = last_parameter.kind != inspect.Parameter.KEYWORD_ONLY
if no_parameter_after_star:
fail(f"Function {self.function.name!r} specifies '*' "
"without any parameters afterwards.")
no_param_after_symbol = True
if no_param_after_symbol:
fname = self.function.full_name
fail(f"Function {fname!r} specifies {symbol!r} "
"without any parameters afterwards.", line_number=lineno)
if self.keyword_only:
check_remaining("*", lambda p: p.kind != inspect.Parameter.KEYWORD_ONLY)
if self.deprecated_positional:
check_remaining("* [from ...]", lambda p: not p.deprecated_positional)
self.function.docstring = self.format_docstring()