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]*/ #[python start generated code]*/
def foo(): pass def foo(): pass
#/*[python checksum:...]*/ #/*[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) fn_with_default_binop_expr_impl(PyObject *module, PyObject *arg)
/*[clinic end generated code: output=018672772e4092ff input=1b55c8ae68d89453]*/ /*[clinic end generated code: output=018672772e4092ff input=1b55c8ae68d89453]*/
/*[python input] /*[python input]
class Custom_converter(CConverter): class Custom_converter(CConverter):
type = "str" type = "str"
@ -5464,3 +5465,812 @@ exit:
static PyObject * static PyObject *
docstr_fallback_to_converter_default_impl(PyObject *module, str a) docstr_fallback_to_converter_default_impl(PyObject *module, str a)
/*[clinic end generated code: output=ae24a9c6f60ee8a6 input=0cbe6a4d24bc2274]*/ /*[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 *",
"module foo\nfoo.bar\n this: int\n *\nDocstring.", "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: for block in dataset:
with self.subTest(block=block): with self.subTest(block=block):
self.expect_failure(block, err) 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): def test_single_slash(self):
block = """ block = """
module foo 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) 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, ...]: def version_splitter(s: str) -> tuple[int, ...]:
"""Splits a version string into a tuple of integers. """Splits a version string into a tuple of integers.
@ -828,6 +835,22 @@ class CLanguage(Language):
#define {methoddef_name} #define {methoddef_name}
#endif /* !defined({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: def __init__(self, filename: str) -> None:
super().__init__(filename) super().__init__(filename)
@ -850,6 +873,64 @@ class CLanguage(Language):
function = o function = o
return self.render_function(clinic, function) 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( def docstring_for_c_string(
self, self,
f: Function f: Function
@ -1199,6 +1280,7 @@ class CLanguage(Language):
flags = 'METH_METHOD|' + flags flags = 'METH_METHOD|' + flags
parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS
deprecated_positionals: dict[int, Parameter] = {}
add_label: str | None = None add_label: str | None = None
for i, p in enumerate(parameters): for i, p in enumerate(parameters):
if isinstance(p.converter, defining_class_converter): if isinstance(p.converter, defining_class_converter):
@ -1213,6 +1295,8 @@ class CLanguage(Language):
parser_code.append("%s:" % add_label) parser_code.append("%s:" % add_label)
add_label = None add_label = None
if not p.is_optional(): if not p.is_optional():
if p.deprecated_positional:
deprecated_positionals[i] = p
parser_code.append(normalize_snippet(parsearg, indent=4)) parser_code.append(normalize_snippet(parsearg, indent=4))
elif i < pos_only: elif i < pos_only:
add_label = 'skip_optional_posonly' add_label = 'skip_optional_posonly'
@ -1242,6 +1326,8 @@ class CLanguage(Language):
goto %s; goto %s;
}} }}
""" % add_label, indent=4)) """ % add_label, indent=4))
if p.deprecated_positional:
deprecated_positionals[i] = p
if i + 1 == len(parameters): if i + 1 == len(parameters):
parser_code.append(normalize_snippet(parsearg, indent=4)) parser_code.append(normalize_snippet(parsearg, indent=4))
else: else:
@ -1257,6 +1343,12 @@ class CLanguage(Language):
}} }}
""" % add_label, indent=4)) """ % 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 parser_code is not None:
if add_label: if add_label:
parser_code.append("%s:" % add_label) parser_code.append("%s:" % add_label)
@ -2592,6 +2684,9 @@ class Function:
return f return f
VersionTuple = tuple[int, int]
@dc.dataclass(repr=False, slots=True) @dc.dataclass(repr=False, slots=True)
class Parameter: class Parameter:
""" """
@ -2606,6 +2701,8 @@ class Parameter:
annotation: object = inspect.Parameter.empty annotation: object = inspect.Parameter.empty
docstring: str = '' docstring: str = ''
group: int = 0 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) right_bracket_count: int = dc.field(init=False, default=0)
def __repr__(self) -> str: def __repr__(self) -> str:
@ -4430,6 +4527,7 @@ class DSLParser:
state: StateKeeper state: StateKeeper
keyword_only: bool keyword_only: bool
positional_only: bool positional_only: bool
deprecated_positional: VersionTuple | None
group: int group: int
parameter_state: ParamState parameter_state: ParamState
indent: IndentStack indent: IndentStack
@ -4437,6 +4535,11 @@ class DSLParser:
coexist: bool coexist: bool
parameter_continuation: str parameter_continuation: str
preserve_output: bool preserve_output: bool
star_from_version_re = create_regex(
before="* [from ",
after="]",
word=False,
)
def __init__(self, clinic: Clinic) -> None: def __init__(self, clinic: Clinic) -> None:
self.clinic = clinic self.clinic = clinic
@ -4460,6 +4563,7 @@ class DSLParser:
self.state = self.state_dsl_start self.state = self.state_dsl_start
self.keyword_only = False self.keyword_only = False
self.positional_only = False self.positional_only = False
self.deprecated_positional = None
self.group = 0 self.group = 0
self.parameter_state: ParamState = ParamState.START self.parameter_state: ParamState = ParamState.START
self.indent = IndentStack() self.indent = IndentStack()
@ -4622,7 +4726,7 @@ class DSLParser:
exc.lineno = line_number exc.lineno = line_number
raise 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)) block.output.extend(self.clinic.language.render(self.clinic, block.signatures))
if self.preserve_output: if self.preserve_output:
@ -4908,8 +5012,14 @@ class DSLParser:
self.parameter_continuation = line[:-1] self.parameter_continuation = line[:-1]
return 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 func = self.function
match line.lstrip(): match line:
case '*': case '*':
self.parse_star(func) self.parse_star(func)
case '[': case '[':
@ -5182,7 +5292,9 @@ class DSLParser:
"after 'self'.") "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()] names = [k.name for k in self.function.parameters.values()]
if parameter_name in names[1:]: if parameter_name in names[1:]:
@ -5215,10 +5327,28 @@ class DSLParser:
"Annotations must be either a name, a function call, or a string." "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: def parse_star(self, function: Function) -> None:
"""Parse keyword-only parameter marker '*'.""" """Parse keyword-only parameter marker '*'."""
if self.keyword_only: if self.keyword_only:
fail(f"Function {function.name!r} uses '*' more than once.") fail(f"Function {function.name!r} uses '*' more than once.")
self.deprecated_positional = None
self.keyword_only = True self.keyword_only = True
def parse_opening_square_bracket(self, function: Function) -> None: def parse_opening_square_bracket(self, function: Function) -> None:
@ -5586,23 +5716,34 @@ class DSLParser:
return docstring 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. Called when processing the block is done.
""" """
if not self.function: if not self.function:
return return
if self.keyword_only: def check_remaining(
values = self.function.parameters.values() symbol: str,
if not values: condition: Callable[[Parameter], bool]
no_parameter_after_star = True ) -> 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: else:
last_parameter = next(reversed(list(values))) no_param_after_symbol = True
no_parameter_after_star = last_parameter.kind != inspect.Parameter.KEYWORD_ONLY if no_param_after_symbol:
if no_parameter_after_star: fname = self.function.full_name
fail(f"Function {self.function.name!r} specifies '*' " fail(f"Function {fname!r} specifies {symbol!r} "
"without any parameters afterwards.") "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() self.function.docstring = self.format_docstring()