]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-95065: Add Argument Clinic support for deprecating positional use of parameters...
authorErlend E. Aasland <erlend@python.org>
Mon, 7 Aug 2023 11:28:08 +0000 (13:28 +0200)
committerGitHub <noreply@github.com>
Mon, 7 Aug 2023 11:28:08 +0000 (11:28 +0000)
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>
Doc/howto/clinic.rst
Lib/test/clinic.test.c
Lib/test/test_clinic.py
Misc/NEWS.d/next/Tools-Demos/2022-07-23-00-33-28.gh-issue-95065.NfCCpp.rst [new file with mode: 0644]
Tools/clinic/clinic.py

index e8e6aace350e0c3746741852b994597ea47db0a9..286623c24101457f97ca9af2e08b09bc0cc030c4 100644 (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.
index d2ad1a0482c304eae237d1f62745dc7728175403..321ac69273189fcbda67bd5d21cac9f56f744c56 100644 (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]*/
index f30fad2126940bf862e05aa9c1de31d5ef6c8ec4..f594e39a90546acb9ed9a4a1cb93344232174c22 100644 (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
diff --git a/Misc/NEWS.d/next/Tools-Demos/2022-07-23-00-33-28.gh-issue-95065.NfCCpp.rst b/Misc/NEWS.d/next/Tools-Demos/2022-07-23-00-33-28.gh-issue-95065.NfCCpp.rst
new file mode 100644 (file)
index 0000000..3641716
--- /dev/null
@@ -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.
index 47b5f5ae32f58161f0d871aaeba1e836437b4596..4dfe90b314f543558ae2e5014ee68689bf776aed 100755 (executable)
@@ -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()