]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-95065, gh-107704: Argument Clinic: support multiple '/ [from ...]' and '* [from...
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 21 Aug 2023 13:59:58 +0000 (16:59 +0300)
committerGitHub <noreply@github.com>
Mon, 21 Aug 2023 13:59:58 +0000 (13:59 +0000)
Include/internal/pycore_global_objects_fini_generated.h
Include/internal/pycore_global_strings.h
Include/internal/pycore_runtime_init_generated.h
Include/internal/pycore_unicodeobject_generated.h
Lib/test/test_clinic.py
Modules/_testclinic.c
Modules/clinic/_testclinic_depr.c.h
Tools/clinic/clinic.py

index ee9010583ff8b58a90838147ad1fafccd139f58f..2f930babf6aad406a021714576d0cc2f3f5a50f2 100644 (file)
@@ -923,6 +923,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(exp));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extend));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(extra_tokens));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(f));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(facility));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(factory));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(false));
@@ -954,6 +955,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(fset));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(func));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(future));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(g));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(generation));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(genexpr));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(get));
@@ -967,6 +969,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(globals));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(groupindex));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(groups));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(h));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(handle));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(hash_name));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(header));
index b081c0e023fa4a26f820a591f32fad9ea1e65c95..5a0cd1a02ba5615bda55c6d56a5bd3811f80691c 100644 (file)
@@ -412,6 +412,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(exp)
         STRUCT_FOR_ID(extend)
         STRUCT_FOR_ID(extra_tokens)
+        STRUCT_FOR_ID(f)
         STRUCT_FOR_ID(facility)
         STRUCT_FOR_ID(factory)
         STRUCT_FOR_ID(false)
@@ -443,6 +444,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(fset)
         STRUCT_FOR_ID(func)
         STRUCT_FOR_ID(future)
+        STRUCT_FOR_ID(g)
         STRUCT_FOR_ID(generation)
         STRUCT_FOR_ID(genexpr)
         STRUCT_FOR_ID(get)
@@ -456,6 +458,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(globals)
         STRUCT_FOR_ID(groupindex)
         STRUCT_FOR_ID(groups)
+        STRUCT_FOR_ID(h)
         STRUCT_FOR_ID(handle)
         STRUCT_FOR_ID(hash_name)
         STRUCT_FOR_ID(header)
index 8c9c7f753d8579d088b6a0609ed92053b7657bf5..8c0fcdb1bdae1c1503b5839443f32e0a830a8861 100644 (file)
@@ -918,6 +918,7 @@ extern "C" {
     INIT_ID(exp), \
     INIT_ID(extend), \
     INIT_ID(extra_tokens), \
+    INIT_ID(f), \
     INIT_ID(facility), \
     INIT_ID(factory), \
     INIT_ID(false), \
@@ -949,6 +950,7 @@ extern "C" {
     INIT_ID(fset), \
     INIT_ID(func), \
     INIT_ID(future), \
+    INIT_ID(g), \
     INIT_ID(generation), \
     INIT_ID(genexpr), \
     INIT_ID(get), \
@@ -962,6 +964,7 @@ extern "C" {
     INIT_ID(globals), \
     INIT_ID(groupindex), \
     INIT_ID(groups), \
+    INIT_ID(h), \
     INIT_ID(handle), \
     INIT_ID(hash_name), \
     INIT_ID(header), \
index 59f40075f93983bbab1bea7d52280040b2436983..841eb78701127102cf22613e434de6edc0ebff8e 100644 (file)
@@ -1077,6 +1077,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     string = &_Py_ID(extra_tokens);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
+    string = &_Py_ID(f);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    _PyUnicode_InternInPlace(interp, &string);
     string = &_Py_ID(facility);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
@@ -1170,6 +1173,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     string = &_Py_ID(future);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
+    string = &_Py_ID(g);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    _PyUnicode_InternInPlace(interp, &string);
     string = &_Py_ID(generation);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
@@ -1209,6 +1215,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
     string = &_Py_ID(groups);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
+    string = &_Py_ID(h);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    _PyUnicode_InternInPlace(interp, &string);
     string = &_Py_ID(handle);
     assert(_PyUnicode_CheckConsistency(string, 1));
     _PyUnicode_InternInPlace(interp, &string);
index 934c1e3ffccca891e998f22bbdb9604d0115042b..310615083778f78a015eebd9eae816c7a53d011c 100644 (file)
@@ -1751,7 +1751,7 @@ class ClinicParserTest(TestCase):
                 * [from 3.14]
             Docstring.
         """
-        err = "Function 'bar': '* [from ...]' must come before '*'"
+        err = "Function 'bar': '* [from ...]' must precede '*'"
         self.expect_failure(block, err, lineno=4)
 
     def test_depr_star_duplicate(self):
@@ -1765,7 +1765,7 @@ class ClinicParserTest(TestCase):
                 c: int
             Docstring.
         """
-        err = "Function 'bar' uses '* [from ...]' more than once."
+        err = "Function 'bar' uses '* [from 3.14]' more than once."
         self.expect_failure(block, err, lineno=5)
 
     def test_depr_star_duplicate2(self):
@@ -1779,7 +1779,7 @@ class ClinicParserTest(TestCase):
                 c: int
             Docstring.
         """
-        err = "Function 'bar' uses '* [from ...]' more than once."
+        err = "Function 'bar': '* [from 3.15]' must precede '* [from 3.14]'"
         self.expect_failure(block, err, lineno=5)
 
     def test_depr_slash_duplicate(self):
@@ -1793,7 +1793,7 @@ class ClinicParserTest(TestCase):
                 c: int
             Docstring.
         """
-        err = "Function 'bar' uses '/ [from ...]' more than once."
+        err = "Function 'bar' uses '/ [from 3.14]' more than once."
         self.expect_failure(block, err, lineno=5)
 
     def test_depr_slash_duplicate2(self):
@@ -1801,13 +1801,13 @@ class ClinicParserTest(TestCase):
             module foo
             foo.bar
                 a: int
-                / [from 3.14]
-                b: int
                 / [from 3.15]
+                b: int
+                / [from 3.14]
                 c: int
             Docstring.
         """
-        err = "Function 'bar' uses '/ [from ...]' more than once."
+        err = "Function 'bar': '/ [from 3.14]' must precede '/ [from 3.15]'"
         self.expect_failure(block, err, lineno=5)
 
     def test_single_slash(self):
@@ -2724,7 +2724,15 @@ class ClinicFunctionalTest(unittest.TestCase):
     locals().update((name, getattr(ac_tester, name))
                     for name in dir(ac_tester) if name.startswith('test_'))
 
-    def check_depr_star(self, pnames, fn, *args, name=None, **kwds):
+    def check_depr(self, regex, fn, /, *args, **kwds):
+        with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
+            # Record the line number, so we're sure we've got the correct stack
+            # level on the deprecation warning.
+            _, lineno = fn(*args, **kwds), sys._getframe().f_lineno
+        self.assertEqual(cm.filename, __file__)
+        self.assertEqual(cm.lineno, lineno)
+
+    def check_depr_star(self, pnames, fn, /, *args, name=None, **kwds):
         if name is None:
             name = fn.__qualname__
             if isinstance(fn, type):
@@ -2734,12 +2742,7 @@ class ClinicFunctionalTest(unittest.TestCase):
             fr"{re.escape(name)}\(\) is deprecated. Parameters? {pnames} will "
             fr"become( a)? keyword-only parameters? in Python 3\.14"
         )
-        with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
-            # Record the line number, so we're sure we've got the correct stack
-            # level on the deprecation warning.
-            _, lineno = fn(*args, **kwds), sys._getframe().f_lineno
-        self.assertEqual(cm.filename, __file__)
-        self.assertEqual(cm.lineno, lineno)
+        self.check_depr(regex, fn, *args, **kwds)
 
     def check_depr_kwd(self, pnames, fn, *args, name=None, **kwds):
         if name is None:
@@ -2749,15 +2752,10 @@ class ClinicFunctionalTest(unittest.TestCase):
         pl = 's' if ' ' in pnames else ''
         regex = (
             fr"Passing keyword argument{pl} {pnames} to "
-            fr"{re.escape(name)}\(\) is deprecated. Corresponding parameter{pl} "
+            fr"{re.escape(name)}\(\) is deprecated. Parameter{pl} {pnames} "
             fr"will become positional-only in Python 3\.14."
         )
-        with self.assertWarnsRegex(DeprecationWarning, regex) as cm:
-            # Record the line number, so we're sure we've got the correct stack
-            # level on the deprecation warning.
-            _, lineno = fn(*args, **kwds), sys._getframe().f_lineno
-        self.assertEqual(cm.filename, __file__)
-        self.assertEqual(cm.lineno, lineno)
+        self.check_depr(regex, fn, *args, **kwds)
 
     def test_objects_converter(self):
         with self.assertRaises(TypeError):
@@ -3368,6 +3366,24 @@ class ClinicFunctionalTest(unittest.TestCase):
         check("a", "b", c="c")
         self.assertRaises(TypeError, fn, "a", "b", "c", "d")
 
+    def test_depr_star_multi(self):
+        fn = ac_tester.depr_star_multi
+        self.assertRaises(TypeError, fn, "a")
+        fn("a", b="b", c="c", d="d", e="e", f="f", g="g", h="h")
+        errmsg = (
+            "Passing more than 1 positional argument to depr_star_multi() is deprecated. "
+            "Parameter 'b' will become a keyword-only parameter in Python 3.16. "
+            "Parameters 'c' and 'd' will become keyword-only parameters in Python 3.15. "
+            "Parameters 'e', 'f' and 'g' will become keyword-only parameters in Python 3.14.")
+        check = partial(self.check_depr, re.escape(errmsg), fn)
+        check("a", "b", c="c", d="d", e="e", f="f", g="g", h="h")
+        check("a", "b", "c", d="d", e="e", f="f", g="g", h="h")
+        check("a", "b", "c", "d", e="e", f="f", g="g", h="h")
+        check("a", "b", "c", "d", "e", f="f", g="g", h="h")
+        check("a", "b", "c", "d", "e", "f", g="g", h="h")
+        check("a", "b", "c", "d", "e", "f", "g", h="h")
+        self.assertRaises(TypeError, fn, "a", "b", "c", "d", "e", "f", "g", "h")
+
     def test_depr_kwd_required_1(self):
         fn = ac_tester.depr_kwd_required_1
         fn("a", "b")
@@ -3452,6 +3468,44 @@ class ClinicFunctionalTest(unittest.TestCase):
         self.assertRaises(TypeError, fn, "a", c="c")
         self.assertRaises(TypeError, fn, a="a", b="b", c="c")
 
+    def test_depr_kwd_multi(self):
+        fn = ac_tester.depr_kwd_multi
+        fn("a", "b", "c", "d", "e", "f", "g", h="h")
+        errmsg = (
+            "Passing keyword arguments 'b', 'c', 'd', 'e', 'f' and 'g' to depr_kwd_multi() is deprecated. "
+            "Parameter 'b' will become positional-only in Python 3.14. "
+            "Parameters 'c' and 'd' will become positional-only in Python 3.15. "
+            "Parameters 'e', 'f' and 'g' will become positional-only in Python 3.16.")
+        check = partial(self.check_depr, re.escape(errmsg), fn)
+        check("a", "b", "c", "d", "e", "f", g="g", h="h")
+        check("a", "b", "c", "d", "e", f="f", g="g", h="h")
+        check("a", "b", "c", "d", e="e", f="f", g="g", h="h")
+        check("a", "b", "c", d="d", e="e", f="f", g="g", h="h")
+        check("a", "b", c="c", d="d", e="e", f="f", g="g", h="h")
+        check("a", b="b", c="c", d="d", e="e", f="f", g="g", h="h")
+        self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g", h="h")
+
+    def test_depr_multi(self):
+        fn = ac_tester.depr_multi
+        self.assertRaises(TypeError, fn, "a", "b", "c", "d", "e", "f", "g")
+        errmsg = (
+            "Passing more than 4 positional arguments to depr_multi() is deprecated. "
+            "Parameter 'e' will become a keyword-only parameter in Python 3.15. "
+            "Parameter 'f' will become a keyword-only parameter in Python 3.14.")
+        check = partial(self.check_depr, re.escape(errmsg), fn)
+        check("a", "b", "c", "d", "e", "f", g="g")
+        check("a", "b", "c", "d", "e", f="f", g="g")
+        fn("a", "b", "c", "d", e="e", f="f", g="g")
+        fn("a", "b", "c", d="d", e="e", f="f", g="g")
+        errmsg = (
+            "Passing keyword arguments 'b' and 'c' to depr_multi() is deprecated. "
+            "Parameter 'b' will become positional-only in Python 3.14. "
+            "Parameter 'c' will become positional-only in Python 3.15.")
+        check = partial(self.check_depr, re.escape(errmsg), fn)
+        check("a", "b", c="c", d="d", e="e", f="f", g="g")
+        check("a", b="b", c="c", d="d", e="e", f="f", g="g")
+        self.assertRaises(TypeError, fn, a="a", b="b", c="c", d="d", e="e", f="f", g="g")
+
 
 class PermutationTests(unittest.TestCase):
     """Test permutation support functions."""
index efec04d99029bb107af53957c5114c9dac6f60bf..2e0535d52641e0df73d8097ce88d53f34583c94c 100644 (file)
@@ -1580,6 +1580,32 @@ depr_star_noinline_impl(PyObject *module, PyObject *a, PyObject *b,
 }
 
 
+/*[clinic input]
+depr_star_multi
+    a: object
+    * [from 3.16]
+    b: object
+    * [from 3.15]
+    c: object
+    d: object
+    * [from 3.14]
+    e: object
+    f: object
+    g: object
+    *
+    h: object
+[clinic start generated code]*/
+
+static PyObject *
+depr_star_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                     PyObject *d, PyObject *e, PyObject *f, PyObject *g,
+                     PyObject *h)
+/*[clinic end generated code: output=77681653f4202068 input=3ebd05d888a957ea]*/
+{
+    Py_RETURN_NONE;
+}
+
+
 /*[clinic input]
 depr_kwd_required_1
     a: object
@@ -1702,6 +1728,59 @@ depr_kwd_noinline_impl(PyObject *module, PyObject *a, PyObject *b,
     Py_RETURN_NONE;
 }
 
+
+/*[clinic input]
+depr_kwd_multi
+    a: object
+    /
+    b: object
+    / [from 3.14]
+    c: object
+    d: object
+    / [from 3.15]
+    e: object
+    f: object
+    g: object
+    / [from 3.16]
+    h: object
+[clinic start generated code]*/
+
+static PyObject *
+depr_kwd_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                    PyObject *d, PyObject *e, PyObject *f, PyObject *g,
+                    PyObject *h)
+/*[clinic end generated code: output=ddfbde80fe1942e1 input=7a074e621c79efd7]*/
+{
+    Py_RETURN_NONE;
+}
+
+
+/*[clinic input]
+depr_multi
+    a: object
+    /
+    b: object
+    / [from 3.14]
+    c: object
+    / [from 3.15]
+    d: object
+    * [from 3.15]
+    e: object
+    * [from 3.14]
+    f: object
+    *
+    g: object
+[clinic start generated code]*/
+
+static PyObject *
+depr_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                PyObject *d, PyObject *e, PyObject *f, PyObject *g)
+/*[clinic end generated code: output=f81c92852ca2d4ee input=5b847c5e44bedd02]*/
+{
+    Py_RETURN_NONE;
+}
+
+
 // Reset PY_VERSION_HEX
 #undef PY_VERSION_HEX
 #define PY_VERSION_HEX _SAVED_PY_VERSION
@@ -1779,6 +1858,7 @@ static PyMethodDef tester_methods[] = {
     DEPR_STAR_POS2_LEN2_METHODDEF
     DEPR_STAR_POS2_LEN2_WITH_KWD_METHODDEF
     DEPR_STAR_NOINLINE_METHODDEF
+    DEPR_STAR_MULTI_METHODDEF
     DEPR_KWD_REQUIRED_1_METHODDEF
     DEPR_KWD_REQUIRED_2_METHODDEF
     DEPR_KWD_OPTIONAL_1_METHODDEF
@@ -1786,6 +1866,8 @@ static PyMethodDef tester_methods[] = {
     DEPR_KWD_OPTIONAL_3_METHODDEF
     DEPR_KWD_REQUIRED_OPTIONAL_METHODDEF
     DEPR_KWD_NOINLINE_METHODDEF
+    DEPR_KWD_MULTI_METHODDEF
+    DEPR_MULTI_METHODDEF
     {NULL, NULL}
 };
 
index 661fdaf2a07181022d6ef313fd171fff29348150..b8365bd559b4e7c05c7d4f3caa2026ee398fe7c2 100644 (file)
@@ -419,8 +419,7 @@ PyDoc_STRVAR(depr_kwd_new__doc__,
 "The deprecation message should use the class name instead of __new__.\n"
 "\n"
 "Note: Passing keyword argument \'a\' to _testclinic.DeprKwdNew() is\n"
-"deprecated. Corresponding parameter will become positional-only in\n"
-"Python 3.14.\n"
+"deprecated. Parameter \'a\' will become positional-only in Python 3.14.\n"
 "");
 
 static PyObject *
@@ -479,8 +478,8 @@ depr_kwd_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
     if (kwargs && PyDict_GET_SIZE(kwargs) && nargs < 1 && fastargs[0]) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword argument 'a' to _testclinic.DeprKwdNew() is "
-                "deprecated. Corresponding parameter will become positional-only "
-                "in Python 3.14.", 1))
+                "deprecated. Parameter 'a' will become positional-only in Python "
+                "3.14.", 1))
         {
             goto exit;
         }
@@ -503,8 +502,7 @@ PyDoc_STRVAR(depr_kwd_init__doc__,
 "The deprecation message should use the class name instead of __init__.\n"
 "\n"
 "Note: Passing keyword argument \'a\' to _testclinic.DeprKwdInit() is\n"
-"deprecated. Corresponding parameter will become positional-only in\n"
-"Python 3.14.\n"
+"deprecated. Parameter \'a\' will become positional-only in Python 3.14.\n"
 "");
 
 static int
@@ -563,8 +561,8 @@ depr_kwd_init(PyObject *self, PyObject *args, PyObject *kwargs)
     if (kwargs && PyDict_GET_SIZE(kwargs) && nargs < 1 && fastargs[0]) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword argument 'a' to _testclinic.DeprKwdInit() is "
-                "deprecated. Corresponding parameter will become positional-only "
-                "in Python 3.14.", 1))
+                "deprecated. Parameter 'a' will become positional-only in Python "
+                "3.14.", 1))
         {
             goto exit;
         }
@@ -641,8 +639,8 @@ depr_kwd_init_noinline(PyObject *self, PyObject *args, PyObject *kwargs)
         }
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'b' and 'c' to "
-                "_testclinic.DeprKwdInitNoInline() is deprecated. Corresponding "
-                "parameters will become positional-only in Python 3.14.", 1))
+                "_testclinic.DeprKwdInitNoInline() is deprecated. Parameters 'b' "
+                "and 'c' will become positional-only in Python 3.14.", 1))
         {
             goto exit;
         }
@@ -1482,13 +1480,110 @@ exit:
     return return_value;
 }
 
+PyDoc_STRVAR(depr_star_multi__doc__,
+"depr_star_multi($module, /, a, b, c, d, e, f, g, *, h)\n"
+"--\n"
+"\n"
+"Note: Passing more than 1 positional argument to depr_star_multi() is\n"
+"deprecated. Parameter \'b\' will become a keyword-only parameter in\n"
+"Python 3.16. Parameters \'c\' and \'d\' will become keyword-only\n"
+"parameters in Python 3.15. Parameters \'e\', \'f\' and \'g\' will become\n"
+"keyword-only parameters in Python 3.14.\n"
+"");
+
+#define DEPR_STAR_MULTI_METHODDEF    \
+    {"depr_star_multi", _PyCFunction_CAST(depr_star_multi), METH_FASTCALL|METH_KEYWORDS, depr_star_multi__doc__},
+
+static PyObject *
+depr_star_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                     PyObject *d, PyObject *e, PyObject *f, PyObject *g,
+                     PyObject *h);
+
+// Emit compiler warnings when we get to Python 3.14.
+#if PY_VERSION_HEX >= 0x030e00C0
+#  error "Update the clinic input of 'depr_star_multi'."
+#elif PY_VERSION_HEX >= 0x030e00A0
+#  ifdef _MSC_VER
+#    pragma message ("Update the clinic input of 'depr_star_multi'.")
+#  else
+#    warning "Update the clinic input of 'depr_star_multi'."
+#  endif
+#endif
+
+static PyObject *
+depr_star_multi(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 8
+    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), &_Py_ID(f), &_Py_ID(g), &_Py_ID(h), },
+    };
+    #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", "f", "g", "h", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "depr_star_multi",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[8];
+    PyObject *a;
+    PyObject *b;
+    PyObject *c;
+    PyObject *d;
+    PyObject *e;
+    PyObject *f;
+    PyObject *g;
+    PyObject *h;
+
+    if (nargs > 1 && nargs <= 7) {
+        if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                "Passing more than 1 positional argument to depr_star_multi() is "
+                "deprecated. Parameter 'b' will become a keyword-only parameter "
+                "in Python 3.16. Parameters 'c' and 'd' will become keyword-only "
+                "parameters in Python 3.15. Parameters 'e', 'f' and 'g' will "
+                "become keyword-only parameters in Python 3.14.", 1))
+        {
+            goto exit;
+        }
+    }
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 7, 7, 1, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    a = args[0];
+    b = args[1];
+    c = args[2];
+    d = args[3];
+    e = args[4];
+    f = args[5];
+    g = args[6];
+    h = args[7];
+    return_value = depr_star_multi_impl(module, a, b, c, d, e, f, g, h);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(depr_kwd_required_1__doc__,
 "depr_kwd_required_1($module, a, /, b)\n"
 "--\n"
 "\n"
 "Note: Passing keyword argument \'b\' to depr_kwd_required_1() is\n"
-"deprecated. Corresponding parameter will become positional-only in\n"
-"Python 3.14.\n"
+"deprecated. Parameter \'b\' will become positional-only in Python 3.14.\n"
 "");
 
 #define DEPR_KWD_REQUIRED_1_METHODDEF    \
@@ -1548,8 +1643,8 @@ depr_kwd_required_1(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P
     if (nargs < 2) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword argument 'b' to depr_kwd_required_1() is "
-                "deprecated. Corresponding parameter will become positional-only "
-                "in Python 3.14.", 1))
+                "deprecated. Parameter 'b' will become positional-only in Python "
+                "3.14.", 1))
         {
             goto exit;
         }
@@ -1567,7 +1662,7 @@ PyDoc_STRVAR(depr_kwd_required_2__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword arguments \'b\' and \'c\' to depr_kwd_required_2()\n"
-"is deprecated. Corresponding parameters will become positional-only in\n"
+"is deprecated. Parameters \'b\' and \'c\' will become positional-only in\n"
 "Python 3.14.\n"
 "");
 
@@ -1630,7 +1725,7 @@ depr_kwd_required_2(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P
     if (nargs < 3) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'b' and 'c' to depr_kwd_required_2() "
-                "is deprecated. Corresponding parameters will become "
+                "is deprecated. Parameters 'b' and 'c' will become "
                 "positional-only in Python 3.14.", 1))
         {
             goto exit;
@@ -1650,8 +1745,7 @@ PyDoc_STRVAR(depr_kwd_optional_1__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword argument \'b\' to depr_kwd_optional_1() is\n"
-"deprecated. Corresponding parameter will become positional-only in\n"
-"Python 3.14.\n"
+"deprecated. Parameter \'b\' will become positional-only in Python 3.14.\n"
 "");
 
 #define DEPR_KWD_OPTIONAL_1_METHODDEF    \
@@ -1712,8 +1806,8 @@ depr_kwd_optional_1(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P
     if (kwnames && PyTuple_GET_SIZE(kwnames) && nargs < 2 && args[1]) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword argument 'b' to depr_kwd_optional_1() is "
-                "deprecated. Corresponding parameter will become positional-only "
-                "in Python 3.14.", 1))
+                "deprecated. Parameter 'b' will become positional-only in Python "
+                "3.14.", 1))
         {
             goto exit;
         }
@@ -1735,7 +1829,7 @@ PyDoc_STRVAR(depr_kwd_optional_2__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword arguments \'b\' and \'c\' to depr_kwd_optional_2()\n"
-"is deprecated. Corresponding parameters will become positional-only in\n"
+"is deprecated. Parameters \'b\' and \'c\' will become positional-only in\n"
 "Python 3.14.\n"
 "");
 
@@ -1799,7 +1893,7 @@ depr_kwd_optional_2(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P
     if (kwnames && PyTuple_GET_SIZE(kwnames) && ((nargs < 2 && args[1]) || (nargs < 3 && args[2]))) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'b' and 'c' to depr_kwd_optional_2() "
-                "is deprecated. Corresponding parameters will become "
+                "is deprecated. Parameters 'b' and 'c' will become "
                 "positional-only in Python 3.14.", 1))
         {
             goto exit;
@@ -1828,7 +1922,7 @@ PyDoc_STRVAR(depr_kwd_optional_3__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword arguments \'a\', \'b\' and \'c\' to\n"
-"depr_kwd_optional_3() is deprecated. Corresponding parameters will\n"
+"depr_kwd_optional_3() is deprecated. Parameters \'a\', \'b\' and \'c\' will\n"
 "become positional-only in Python 3.14.\n"
 "");
 
@@ -1892,8 +1986,8 @@ depr_kwd_optional_3(PyObject *module, PyObject *const *args, Py_ssize_t nargs, P
     if (kwnames && PyTuple_GET_SIZE(kwnames) && ((nargs < 1 && args[0]) || (nargs < 2 && args[1]) || (nargs < 3 && args[2]))) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'a', 'b' and 'c' to "
-                "depr_kwd_optional_3() is deprecated. Corresponding parameters "
-                "will become positional-only in Python 3.14.", 1))
+                "depr_kwd_optional_3() is deprecated. Parameters 'a', 'b' and 'c'"
+                " will become positional-only in Python 3.14.", 1))
         {
             goto exit;
         }
@@ -1926,7 +2020,7 @@ PyDoc_STRVAR(depr_kwd_required_optional__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword arguments \'b\' and \'c\' to\n"
-"depr_kwd_required_optional() is deprecated. Corresponding parameters\n"
+"depr_kwd_required_optional() is deprecated. Parameters \'b\' and \'c\'\n"
 "will become positional-only in Python 3.14.\n"
 "");
 
@@ -1990,8 +2084,8 @@ depr_kwd_required_optional(PyObject *module, PyObject *const *args, Py_ssize_t n
     if (kwnames && PyTuple_GET_SIZE(kwnames) && ((nargs < 2) || (nargs < 3 && args[2]))) {
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'b' and 'c' to "
-                "depr_kwd_required_optional() is deprecated. Corresponding "
-                "parameters will become positional-only in Python 3.14.", 1))
+                "depr_kwd_required_optional() is deprecated. Parameters 'b' and "
+                "'c' will become positional-only in Python 3.14.", 1))
         {
             goto exit;
         }
@@ -2014,7 +2108,7 @@ PyDoc_STRVAR(depr_kwd_noinline__doc__,
 "--\n"
 "\n"
 "Note: Passing keyword arguments \'b\' and \'c\' to depr_kwd_noinline() is\n"
-"deprecated. Corresponding parameters will become positional-only in\n"
+"deprecated. Parameters \'b\' and \'c\' will become positional-only in\n"
 "Python 3.14.\n"
 "");
 
@@ -2081,8 +2175,8 @@ depr_kwd_noinline(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyO
         }
         if (PyErr_WarnEx(PyExc_DeprecationWarning,
                 "Passing keyword arguments 'b' and 'c' to depr_kwd_noinline() is "
-                "deprecated. Corresponding parameters will become positional-only"
-                " in Python 3.14.", 1))
+                "deprecated. Parameters 'b' and 'c' will become positional-only "
+                "in Python 3.14.", 1))
         {
             goto exit;
         }
@@ -2092,4 +2186,209 @@ depr_kwd_noinline(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyO
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=fc558c1efdcab076 input=a9049054013a1b77]*/
+
+PyDoc_STRVAR(depr_kwd_multi__doc__,
+"depr_kwd_multi($module, a, /, b, c, d, e, f, g, h)\n"
+"--\n"
+"\n"
+"Note: Passing keyword arguments \'b\', \'c\', \'d\', \'e\', \'f\' and \'g\' to\n"
+"depr_kwd_multi() is deprecated. Parameter \'b\' will become positional-\n"
+"only in Python 3.14. Parameters \'c\' and \'d\' will become positional-\n"
+"only in Python 3.15. Parameters \'e\', \'f\' and \'g\' will become\n"
+"positional-only in Python 3.16.\n"
+"");
+
+#define DEPR_KWD_MULTI_METHODDEF    \
+    {"depr_kwd_multi", _PyCFunction_CAST(depr_kwd_multi), METH_FASTCALL|METH_KEYWORDS, depr_kwd_multi__doc__},
+
+static PyObject *
+depr_kwd_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                    PyObject *d, PyObject *e, PyObject *f, PyObject *g,
+                    PyObject *h);
+
+// Emit compiler warnings when we get to Python 3.14.
+#if PY_VERSION_HEX >= 0x030e00C0
+#  error "Update the clinic input of 'depr_kwd_multi'."
+#elif PY_VERSION_HEX >= 0x030e00A0
+#  ifdef _MSC_VER
+#    pragma message ("Update the clinic input of 'depr_kwd_multi'.")
+#  else
+#    warning "Update the clinic input of 'depr_kwd_multi'."
+#  endif
+#endif
+
+static PyObject *
+depr_kwd_multi(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 7
+    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(b), &_Py_ID(c), &_Py_ID(d), &_Py_ID(e), &_Py_ID(f), &_Py_ID(g), &_Py_ID(h), },
+    };
+    #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[] = {"", "b", "c", "d", "e", "f", "g", "h", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "depr_kwd_multi",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[8];
+    PyObject *a;
+    PyObject *b;
+    PyObject *c;
+    PyObject *d;
+    PyObject *e;
+    PyObject *f;
+    PyObject *g;
+    PyObject *h;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 8, 8, 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (nargs < 7) {
+        if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                "Passing keyword arguments 'b', 'c', 'd', 'e', 'f' and 'g' to "
+                "depr_kwd_multi() is deprecated. Parameter 'b' will become "
+                "positional-only in Python 3.14. Parameters 'c' and 'd' will "
+                "become positional-only in Python 3.15. Parameters 'e', 'f' and "
+                "'g' will become positional-only in Python 3.16.", 1))
+        {
+            goto exit;
+        }
+    }
+    a = args[0];
+    b = args[1];
+    c = args[2];
+    d = args[3];
+    e = args[4];
+    f = args[5];
+    g = args[6];
+    h = args[7];
+    return_value = depr_kwd_multi_impl(module, a, b, c, d, e, f, g, h);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(depr_multi__doc__,
+"depr_multi($module, a, /, b, c, d, e, f, *, g)\n"
+"--\n"
+"\n"
+"Note: Passing keyword arguments \'b\' and \'c\' to depr_multi() is\n"
+"deprecated. Parameter \'b\' will become positional-only in Python 3.14.\n"
+"Parameter \'c\' will become positional-only in Python 3.15.\n"
+"\n"
+"\n"
+"Note: Passing more than 4 positional arguments to depr_multi() is\n"
+"deprecated. Parameter \'e\' will become a keyword-only parameter in\n"
+"Python 3.15. Parameter \'f\' will become a keyword-only parameter in\n"
+"Python 3.14.\n"
+"");
+
+#define DEPR_MULTI_METHODDEF    \
+    {"depr_multi", _PyCFunction_CAST(depr_multi), METH_FASTCALL|METH_KEYWORDS, depr_multi__doc__},
+
+static PyObject *
+depr_multi_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                PyObject *d, PyObject *e, PyObject *f, PyObject *g);
+
+// Emit compiler warnings when we get to Python 3.14.
+#if PY_VERSION_HEX >= 0x030e00C0
+#  error "Update the clinic input of 'depr_multi'."
+#elif PY_VERSION_HEX >= 0x030e00A0
+#  ifdef _MSC_VER
+#    pragma message ("Update the clinic input of 'depr_multi'.")
+#  else
+#    warning "Update the clinic input of 'depr_multi'."
+#  endif
+#endif
+
+static PyObject *
+depr_multi(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 6
+    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(b), &_Py_ID(c), &_Py_ID(d), &_Py_ID(e), &_Py_ID(f), &_Py_ID(g), },
+    };
+    #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[] = {"", "b", "c", "d", "e", "f", "g", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "depr_multi",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[7];
+    PyObject *a;
+    PyObject *b;
+    PyObject *c;
+    PyObject *d;
+    PyObject *e;
+    PyObject *f;
+    PyObject *g;
+
+    if (nargs > 4 && nargs <= 6) {
+        if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                "Passing more than 4 positional arguments to depr_multi() is "
+                "deprecated. Parameter 'e' will become a keyword-only parameter "
+                "in Python 3.15. Parameter 'f' will become a keyword-only "
+                "parameter in Python 3.14.", 1))
+        {
+            goto exit;
+        }
+    }
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 6, 6, 1, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (nargs < 3) {
+        if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                "Passing keyword arguments 'b' and 'c' to depr_multi() is "
+                "deprecated. Parameter 'b' will become positional-only in Python "
+                "3.14. Parameter 'c' will become positional-only in Python 3.15.", 1))
+        {
+            goto exit;
+        }
+    }
+    a = args[0];
+    b = args[1];
+    c = args[2];
+    d = args[3];
+    e = args[4];
+    f = args[5];
+    g = args[6];
+    return_value = depr_multi_impl(module, a, b, c, d, e, f, g);
+
+exit:
+    return return_value;
+}
+/*[clinic end generated code: output=ee8b1933e4bf4dc4 input=a9049054013a1b77]*/
index fe84e810760b4042a59e6c854b5dd60daa59129d..2c6fc4022e3809a032feb4c7655e9bf997620d13 100755 (executable)
@@ -35,6 +35,7 @@ from collections.abc import (
     Iterator,
     Sequence,
 )
+from operator import attrgetter
 from types import FunctionType, NoneType
 from typing import (
     TYPE_CHECKING,
@@ -922,41 +923,38 @@ class CLanguage(Language):
             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
-        major, minor = thenceforth
+        first_pos = next(iter(params))
+        last_pos = next(reversed(params))
 
         # 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 "
-            message = preamble + (
-                f"{func.fulldisplayname}() is deprecated. Parameter {pstr} will "
-                f"become a keyword-only parameter in Python {major}.{minor}."
-            )
+            amount = f"{first_pos+1} " if first_pos else ""
+            pl = "s"
         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 "
+            amount = f"more than {first_pos} " if first_pos else ""
+            pl = "s" if first_pos != 1 else ""
+        message = (
+            f"Passing {amount}positional argument{pl} to "
+            f"{func.fulldisplayname}() is deprecated."
+        )
+
+        for (major, minor), group in itertools.groupby(
+            params.values(), key=attrgetter("deprecated_positional")
+        ):
+            names = [repr(p.name) for p in group]
+            pstr = pprint_words(names)
+            if len(names) == 1:
+                message += (
+                    f" Parameter {pstr} will become a keyword-only parameter "
+                    f"in Python {major}.{minor}."
+                )
+            else:
+                message += (
+                    f" Parameters {pstr} will become keyword-only parameters "
+                    f"in Python {major}.{minor}."
                 )
-            message = preamble + (
-                f"{func.fulldisplayname}() is deprecated. Parameters {pstr} will "
-                f"become keyword-only parameters in Python {major}.{minor}."
-            )
 
         # Append deprecation warning to docstring.
         docstring = textwrap.fill(f"Note: {message}")
@@ -977,19 +975,8 @@ class CLanguage(Language):
             argname_fmt: str | None,
     ) -> str:
         assert len(params) > 0
-        names = [repr(p.name) for p in params.values()]
-        first_param = next(iter(params.values()))
         last_param = next(reversed(params.values()))
 
-        # Pretty-print list of names.
-        pstr = pprint_words(names)
-
-        # For now, assume there's only one deprecation level.
-        assert first_param.deprecated_keyword == last_param.deprecated_keyword
-        thenceforth = first_param.deprecated_keyword
-        assert thenceforth is not None
-        major, minor = thenceforth
-
         # Format the deprecation message.
         containscheck = ""
         conditions = []
@@ -1013,16 +1000,25 @@ class CLanguage(Language):
                 condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
             else:
                 condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
-        if len(params) == 1:
-            what1 = "argument"
-            what2 = "parameter"
-        else:
-            what1 = "arguments"
-            what2 = "parameters"
+        names = [repr(p.name) for p in params.values()]
+        pstr = pprint_words(names)
+        pl = 's' if len(params) != 1 else ''
         message = (
-            f"Passing keyword {what1} {pstr} to {func.fulldisplayname}() is deprecated. "
-            f"Corresponding {what2} will become positional-only in Python {major}.{minor}."
+            f"Passing keyword argument{pl} {pstr} to "
+            f"{func.fulldisplayname}() is deprecated."
         )
+
+        for (major, minor), group in itertools.groupby(
+            params.values(), key=attrgetter("deprecated_keyword")
+        ):
+            names = [repr(p.name) for p in group]
+            pstr = pprint_words(names)
+            pl = 's' if len(names) != 1 else ''
+            message += (
+                f" Parameter{pl} {pstr} will become positional-only "
+                f"in Python {major}.{minor}."
+            )
+
         if containscheck:
             errcheck = f"""
             if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail
@@ -5528,9 +5524,15 @@ class DSLParser:
             self.keyword_only = True
         else:
             if self.keyword_only:
-                fail(f"Function {function.name!r}: '* [from ...]' must come before '*'")
+                fail(f"Function {function.name!r}: '* [from ...]' must precede '*'")
             if self.deprecated_positional:
-                fail(f"Function {function.name!r} uses '* [from ...]' more than once.")
+                if self.deprecated_positional == version:
+                    fail(f"Function {function.name!r} uses '* [from "
+                         f"{version[0]}.{version[1]}]' more than once.")
+                if self.deprecated_positional < version:
+                    fail(f"Function {function.name!r}: '* [from "
+                         f"{version[0]}.{version[1]}]' must precede '* [from "
+                         f"{self.deprecated_positional[0]}.{self.deprecated_positional[1]}]'")
         self.deprecated_positional = version
 
     def parse_opening_square_bracket(self, function: Function) -> None:
@@ -5582,7 +5584,13 @@ class DSLParser:
                 fail(f"Function {function.name!r} uses '/' more than once.")
         else:
             if self.deprecated_keyword:
-                fail(f"Function {function.name!r} uses '/ [from ...]' more than once.")
+                if self.deprecated_keyword == version:
+                    fail(f"Function {function.name!r} uses '/ [from "
+                         f"{version[0]}.{version[1]}]' more than once.")
+                if self.deprecated_keyword > version:
+                    fail(f"Function {function.name!r}: '/ [from "
+                         f"{version[0]}.{version[1]}]' must precede '/ [from "
+                         f"{self.deprecated_keyword[0]}.{self.deprecated_keyword[1]}]'")
             if self.deprecated_positional:
                 fail(f"Function {function.name!r}: '/ [from ...]' must precede '* [from ...]'")
             if self.keyword_only:
@@ -5613,7 +5621,7 @@ class DSLParser:
             if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
                 if version is None:
                     p.kind = inspect.Parameter.POSITIONAL_ONLY
-                else:
+                elif p.deprecated_keyword is None:
                     p.deprecated_keyword = version
 
     def state_parameter_docstring_start(self, line: str) -> None: