https://github.com/python/cpython/commit/01399a3f411462f0fe62ac9700f17914e1efb0ea
commit: 01399a3f411462f0fe62ac9700f17914e1efb0ea
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-13T20:24:02+03:00
summary:

gh-108271, gh-108270: Argument Clinic: parameter aliases and deprecation 
(GH-155248)

A parameter can be given an alternative name by declaring a keyword-only
parameter with a default value which shares the C name of a preceding one:

    a: object = None
    *
    b as a: object = None

Only one of the alternative names can be used in a call; passing both is
a TypeError.  An alias is not shown in the signature.

The `[until X.Y]` prefix marks a parameter which will be removed in that
release.  Passing it emits a DeprecationWarning, and the generated code
warns at compile time when that release is reached.  A deprecated
parameter must have a default value, and only the last positional-only
parameters can be deprecated, because removing one would leave no way to
pass those which follow it.

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-23.gh-issue-108271.Yvq2Tv.rst
A Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-24.gh-issue-108270.kVDJSS.rst
M Lib/test/test_clinic.py
M Modules/_testclinic.c
M Modules/clinic/_testclinic.c.h
M Modules/clinic/_testclinic_depr.c.h
M Tools/clinic/libclinic/clanguage.py
M Tools/clinic/libclinic/converter.py
M Tools/clinic/libclinic/dsl_parser.py
M Tools/clinic/libclinic/function.py
M Tools/clinic/libclinic/parse_args.py

diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index 9145ab1ee26e6f8..20da30f445d28c7 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -15,6 +15,7 @@
 import re
 import sys
 import unittest
+import warnings
 
 test_tools.skip_if_missing('clinic')
 with test_tools.imports_under_tool('clinic'):
@@ -2305,6 +2306,128 @@ def test_depr_slash_duplicate2(self):
         err = "Function 'bar': '/ [from 3.14]' must precede '/ [from 3.15]'"
         self.expect_failure(block, err, lineno=5)
 
+    def test_alias(self):
+        function = self.parse_function("""
+            module foo
+            foo.bar
+                a: int
+                *
+                b as a: int = 0
+            Docstring.
+        """)
+        _, a, b = function.parameters.values()
+        self.assertIsNone(a.converter.alias_of)
+        self.assertIs(b.converter.alias_of, a)
+        self.assertEqual(function.docstring.splitlines()[0],
+                         "bar($module, /, a)")
+
+    def test_alias_must_be_keyword_only(self):
+        block = """
+            module foo
+            foo.bar
+                a: int
+                b as a: int = 0
+            Docstring.
+        """
+        err = "Alias 'b' of the parameter 'a' must be keyword-only."
+        self.expect_failure(block, err, lineno=3)
+
+    def test_alias_must_have_default(self):
+        block = """
+            module foo
+            foo.bar
+                a: int
+                *
+                b as a: int
+            Docstring.
+        """
+        err = "Alias 'b' of the parameter 'a' must have a default value."
+        self.expect_failure(block, err, lineno=4)
+
+    def test_alias_deprecated(self):
+        function = self.parse_function("""
+            module foo
+            foo.bar
+                a: int
+                *
+                [until 3.14] b as a: int = 0
+            Docstring.
+        """)
+        _, a, b = function.parameters.values()
+        self.assertIsNone(a.deprecated_until)
+        self.assertEqual(b.deprecated_until, (3, 14))
+
+    def test_deprecated_last_positional_only_parameters(self):
+        function = self.parse_function("""
+            module foo
+            foo.bar
+                a: int = 0
+                [until 3.14] b: int = 0
+                [until 3.14] c: int = 0
+                /
+                d: int = 0
+            Docstring.
+        """)
+        _, a, b, c, d = function.parameters.values()
+        self.assertIsNone(a.deprecated_until)
+        self.assertEqual(b.deprecated_until, (3, 14))
+        self.assertEqual(c.deprecated_until, (3, 14))
+        self.assertIsNone(d.deprecated_until)
+
+    def test_deprecated_non_last_positional_only_parameter(self):
+        block = """
+            module foo
+            foo.bar
+                [until 3.14] a: int = 0
+                b: int = 0
+                /
+            Docstring.
+        """
+        err = ("Parameter 'b' cannot follow the deprecated parameter 'a': "
+               "only the last positional-only parameters can be deprecated.")
+        self.expect_failure(block, err, lineno=4)
+
+    def test_deprecated_non_positional_only_parameters(self):
+        # The following parameters can still be passed by keyword.
+        function = self.parse_function("""
+            module foo
+            foo.bar
+                [until 3.14] a: int = 0
+                b: int = 0
+                *
+                [until 3.14] c: int = 0
+                d: int = 0
+            Docstring.
+        """)
+        _, a, b, c, d = function.parameters.values()
+        self.assertEqual(a.deprecated_until, (3, 14))
+        self.assertIsNone(b.deprecated_until)
+        self.assertEqual(c.deprecated_until, (3, 14))
+        self.assertIsNone(d.deprecated_until)
+
+    def test_deprecated_parameter_without_default(self):
+        block = """
+            module foo
+            foo.bar
+                [until 3.14] a: int
+            Docstring.
+        """
+        err = "Deprecated parameter 'a' must have a default value."
+        self.expect_failure(block, err, lineno=2)
+
+    def test_deprecated_invalid_format(self):
+        block = """
+            module foo
+            foo.bar
+                [until 3] a: int = 0
+            Docstring.
+        """
+        err = (
+            "Function 'bar': expected format '[until major.minor]' "
+            "where 'major' and 'minor' are integers; got '3'"
+        )
+        self.expect_failure(block, err, lineno=2)
+
     def test_single_slash(self):
         block = """
             module foo
@@ -5072,6 +5195,58 @@ def test_depr_multi(self):
         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")
 
+    def test_alias_pos(self):
+        fn = ac_tester.alias_pos
+        self.assertIsNone(fn())
+        self.assertEqual(fn(1), 1)
+        self.assertEqual(fn(a=1), 1)
+        self.assertEqual(fn(b=1), 1)
+        self.assertEqual(fn.__text_signature__, "($module, /, a=None)")
+        errmsg = re.escape(
+            "argument for alias_pos() given by name ('b') and position (1)")
+        self.assertRaisesRegex(TypeError, errmsg, fn, 1, b=2)
+        errmsg = re.escape(
+            "argument for alias_pos() given by name ('b') and name ('a')")
+        self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)
+
+    def test_alias_kwonly(self):
+        fn = ac_tester.alias_kwonly
+        self.assertIsNone(fn())
+        self.assertEqual(fn(a=1), 1)
+        self.assertEqual(fn(b=1), 1)
+        self.assertEqual(fn.__text_signature__, "($module, /, *, a=None)")
+        self.assertRaises(TypeError, fn, 1)
+        errmsg = re.escape(
+            "argument for alias_kwonly() given by name ('b') and name ('a')")
+        self.assertRaisesRegex(TypeError, errmsg, fn, a=1, b=2)
+
+    def test_depr_alias(self):
+        fn = ac_tester.depr_alias
+        self.assertEqual(fn(1), 1)
+        self.assertEqual(fn(a=1), 1)
+        errmsg = ("Passing the argument 'b' to depr_alias() is deprecated. "
+                  "Use 'a' instead. It will be removed in Python 3.14.")
+        self.check_depr(re.escape(errmsg), fn, b=1)
+
+    def test_depr_param(self):
+        fn = ac_tester.depr_param
+        self.assertEqual(fn(), (None, None, None, None))
+        self.assertEqual(fn(1), (1, None, None, None))
+        def errmsg(name):
+            return re.escape(f"Passing the argument {name!r} to depr_param() "
+                             f"is deprecated. "
+                             f"It will be removed in Python 3.14.")
+        self.check_depr(errmsg('b'), fn, 1, 2)
+        self.check_depr(errmsg('d'), fn, 1, d=4)
+        # Each deprecated parameter is reported on its own.
+        with warnings.catch_warnings(record=True) as caught:
+            warnings.simplefilter("always")
+            self.assertEqual(fn(1, 2, 3), (1, 2, 3, None))
+        self.assertEqual(len(caught), 2)
+        for warning, name in zip(caught, 'bc'):
+            self.assertIs(warning.category, DeprecationWarning)
+            self.assertRegex(str(warning.message), errmsg(name))
+
     def test_lone_kwds(self):
         with self.assertRaises(TypeError):
             ac_tester.lone_kwds(1, 2)
@@ -5266,6 +5441,26 @@ def test_limited_capi_double(self):
         self.assertIn("double f;", generated)
         self.assertIn("f = PyFloat_AsDouble", generated)
 
+    def test_limited_capi_alias(self):
+        block = self.wrap_clinic_input("""
+            func
+                a: object = None
+                *
+                b as a: object = None
+        """)
+        err = ("Parameter 'b' cannot be an alias: "
+               "the arguments are not parsed one by one.")
+        _expect_failure(self, self.clinic.parse, block, err)
+
+    def test_limited_capi_deprecated(self):
+        block = self.wrap_clinic_input("""
+            func
+                [until 3.14] a: object = None
+        """)
+        err = ("Parameter 'a' cannot be deprecated: "
+               "the arguments are not parsed one by one.")
+        _expect_failure(self, self.clinic.parse, block, err)
+
 
 try:
     import _testclinic_limited
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-23.gh-issue-108271.Yvq2Tv.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-23.gh-issue-108271.Yvq2Tv.rst
new file mode 100644
index 000000000000000..3e9d07600223b91
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-23.gh-issue-108271.Yvq2Tv.rst
@@ -0,0 +1,3 @@
+Argument Clinic: add support for parameter aliases.
+A keyword-only parameter with a default value which shares the C name of a
+preceding parameter declares an alternative name for it.
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-24.gh-issue-108270.kVDJSS.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-24.gh-issue-108270.kVDJSS.rst
new file mode 100644
index 000000000000000..07ba709e3898a3e
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-05-18-16-24.gh-issue-108270.kVDJSS.rst
@@ -0,0 +1,3 @@
+Argument Clinic: add support for deprecating a parameter with the ``[until
+X.Y]`` marker.
+Passing such argument emits a :exc:`DeprecationWarning`.
diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c
index 9cbacbd14f86a8a..8b1a547ff297d11 100644
--- a/Modules/_testclinic.c
+++ b/Modules/_testclinic.c
@@ -1523,6 +1523,40 @@ clone_with_conv_f2_impl(PyObject *module, custom_t path)
 }
 
 
+/*[clinic input]
+alias_pos
+
+    a: object = None
+    *
+    b as a: object = None
+
+[clinic start generated code]*/
+
+static PyObject *
+alias_pos_impl(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=f6cd3c7f098a894d input=8018ee6c26e3f435]*/
+{
+    return Py_NewRef(a);
+}
+
+
+/*[clinic input]
+alias_kwonly
+
+    *
+    a: object = None
+    b as a: object = None
+
+[clinic start generated code]*/
+
+static PyObject *
+alias_kwonly_impl(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=9a6d4202ba972f46 input=8ad2d6c0f326571d]*/
+{
+    return Py_NewRef(a);
+}
+
+
 /*[clinic input]
 class _testclinic.TestClass "PyObject *" "&PyBaseObject_Type"
 [clinic start generated code]*/
@@ -2399,6 +2433,40 @@ depr_kwd_multi_impl(PyObject *module, PyObject *a, 
PyObject *b, PyObject *c,
 }
 
 
+/*[clinic input]
+depr_alias
+    a: object = None
+    *
+    [until 3.14] b as a: object = None
+[clinic start generated code]*/
+
+static PyObject *
+depr_alias_impl(PyObject *module, PyObject *a)
+/*[clinic end generated code: output=85e89838716d9423 input=92efd3f244c2ec3f]*/
+{
+    return Py_NewRef(a);
+}
+
+
+/*[clinic input]
+depr_param
+    a: object = None
+    [until 3.14] b: object = None
+    [until 3.14] c: object = None
+    /
+    *
+    [until 3.14] d: object = None
+[clinic start generated code]*/
+
+static PyObject *
+depr_param_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                PyObject *d)
+/*[clinic end generated code: output=5a42b461851c467b input=f689a85166408359]*/
+{
+    return pack_arguments_newref(4, a, b, c, d);
+}
+
+
 /*[clinic input]
 depr_multi
     a: object
@@ -2736,6 +2804,9 @@ static PyMethodDef tester_methods[] = {
     CLONE_WITH_CONV_F1_METHODDEF
     CLONE_WITH_CONV_F2_METHODDEF
 
+    ALIAS_POS_METHODDEF
+    ALIAS_KWONLY_METHODDEF
+
     DEPR_STAR_POS0_LEN1_METHODDEF
     DEPR_STAR_POS0_LEN2_METHODDEF
     DEPR_STAR_POS0_LEN3_WITH_KWD_METHODDEF
@@ -2756,6 +2827,8 @@ static PyMethodDef tester_methods[] = {
     DEPR_KWD_NOINLINE_METHODDEF
     DEPR_KWD_MULTI_METHODDEF
     DEPR_MULTI_METHODDEF
+    DEPR_ALIAS_METHODDEF
+    DEPR_PARAM_METHODDEF
 
     LONE_KWDS_METHODDEF
     KWDS_WITH_POS_ONLY_METHODDEF
diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h
index 9eee8c15fdedf9c..c3bf217a9e7b7b7 100644
--- a/Modules/clinic/_testclinic.c.h
+++ b/Modules/clinic/_testclinic.c.h
@@ -4259,6 +4259,158 @@ clone_with_conv_f2(PyObject *module, PyObject *const 
*args, Py_ssize_t nargs, Py
     return return_value;
 }
 
+PyDoc_STRVAR(alias_pos__doc__,
+"alias_pos($module, /, a=None)\n"
+"--\n"
+"\n");
+
+#define ALIAS_POS_METHODDEF    \
+    {"alias_pos", _PyCFunction_CAST(alias_pos), METH_FASTCALL|METH_KEYWORDS, 
alias_pos__doc__},
+
+static PyObject *
+alias_pos_impl(PyObject *module, PyObject *a);
+
+static PyObject *
+alias_pos(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
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { _Py_LATIN1_CHR('a'), _Py_LATIN1_CHR('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 = "alias_pos",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[2];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 
0;
+    PyObject *a = Py_None;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    if (args[0]) {
+        a = args[0];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+skip_optional_pos:
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (args[0]) {
+        PyErr_Format(PyExc_TypeError,
+                "argument for alias_pos() given by "
+                "name ('b') and %s", 0 < nargs ? "position (1)" : "name 
('a')");
+        goto exit;
+    }
+    a = args[1];
+skip_optional_kwonly:
+    return_value = alias_pos_impl(module, a);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(alias_kwonly__doc__,
+"alias_kwonly($module, /, *, a=None)\n"
+"--\n"
+"\n");
+
+#define ALIAS_KWONLY_METHODDEF    \
+    {"alias_kwonly", _PyCFunction_CAST(alias_kwonly), 
METH_FASTCALL|METH_KEYWORDS, alias_kwonly__doc__},
+
+static PyObject *
+alias_kwonly_impl(PyObject *module, PyObject *a);
+
+static PyObject *
+alias_kwonly(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
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { _Py_LATIN1_CHR('a'), _Py_LATIN1_CHR('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 = "alias_kwonly",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[2];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 
0;
+    PyObject *a = Py_None;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (args[0]) {
+        a = args[0];
+        if (!--noptargs) {
+            goto skip_optional_kwonly;
+        }
+    }
+    if (args[0]) {
+        PyErr_Format(PyExc_TypeError,
+                "argument for alias_kwonly() given by "
+                "name ('b') and name ('a')");
+        goto exit;
+    }
+    a = args[1];
+skip_optional_kwonly:
+    return_value = alias_kwonly_impl(module, a);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(_testclinic_TestClass_get_defining_class__doc__,
 "get_defining_class($self, /)\n"
 "--\n"
@@ -5197,4 +5349,4 @@ vc_kwonly_vectorcall(PyObject *type, PyObject *const 
*args,
         kwnames ? PyTuple_GET_SIZE(kwnames) : 0,
         NULL, kwnames);
 }
-/*[clinic end generated code: output=10fcd30a5d85ce11 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=8a219f606f1296ac input=a9049054013a1b77]*/
diff --git a/Modules/clinic/_testclinic_depr.c.h 
b/Modules/clinic/_testclinic_depr.c.h
index 35f0394e2d3da14..1b0195185329a2a 100644
--- a/Modules/clinic/_testclinic_depr.c.h
+++ b/Modules/clinic/_testclinic_depr.c.h
@@ -2366,6 +2366,212 @@ depr_kwd_multi(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs, PyObje
     return return_value;
 }
 
+PyDoc_STRVAR(depr_alias__doc__,
+"depr_alias($module, /, a=None)\n"
+"--\n"
+"\n");
+
+#define DEPR_ALIAS_METHODDEF    \
+    {"depr_alias", _PyCFunction_CAST(depr_alias), METH_FASTCALL|METH_KEYWORDS, 
depr_alias__doc__},
+
+static PyObject *
+depr_alias_impl(PyObject *module, PyObject *a);
+
+// Emit compiler warnings when we get to Python 3.14.
+#if PY_VERSION_HEX >= 0x030e00C0
+#  error "Update the clinic input of 'depr_alias'."
+#elif PY_VERSION_HEX >= 0x030e00A0
+#  ifdef _MSC_VER
+#    pragma message ("Update the clinic input of 'depr_alias'.")
+#  else
+#    warning "Update the clinic input of 'depr_alias'."
+#  endif
+#endif
+
+static PyObject *
+depr_alias(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
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { _Py_LATIN1_CHR('a'), _Py_LATIN1_CHR('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 = "depr_alias",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[2];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 
0;
+    PyObject *a = Py_None;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (!noptargs) {
+        goto skip_optional_pos;
+    }
+    if (args[0]) {
+        a = args[0];
+        if (!--noptargs) {
+            goto skip_optional_pos;
+        }
+    }
+skip_optional_pos:
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (args[0]) {
+        PyErr_Format(PyExc_TypeError,
+                "argument for depr_alias() given by "
+                "name ('b') and %s", 0 < nargs ? "position (1)" : "name 
('a')");
+        goto exit;
+    }
+    if (PyErr_WarnEx(PyExc_DeprecationWarning,
+            "Passing the argument 'b' to depr_alias() is deprecated. Use 'a' "
+            "instead. It will be removed in Python 3.14.", 1))
+    {
+        goto exit;
+    }
+    a = args[1];
+skip_optional_kwonly:
+    return_value = depr_alias_impl(module, a);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(depr_param__doc__,
+"depr_param($module, a=None, b=None, c=None, /, *, d=None)\n"
+"--\n"
+"\n");
+
+#define DEPR_PARAM_METHODDEF    \
+    {"depr_param", _PyCFunction_CAST(depr_param), METH_FASTCALL|METH_KEYWORDS, 
depr_param__doc__},
+
+static PyObject *
+depr_param_impl(PyObject *module, PyObject *a, PyObject *b, PyObject *c,
+                PyObject *d);
+
+// Emit compiler warnings when we get to Python 3.14.
+#if PY_VERSION_HEX >= 0x030e00C0
+#  error "Update the clinic input of 'depr_param'."
+#elif PY_VERSION_HEX >= 0x030e00A0
+#  ifdef _MSC_VER
+#    pragma message ("Update the clinic input of 'depr_param'.")
+#  else
+#    warning "Update the clinic input of 'depr_param'."
+#  endif
+#endif
+
+static PyObject *
+depr_param(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
+        Py_hash_t ob_hash;
+        PyObject *ob_item[NUM_KEYWORDS];
+    } _kwtuple = {
+        .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+        .ob_hash = -1,
+        .ob_item = { _Py_LATIN1_CHR('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[] = {"", "", "", "d", NULL};
+    static _PyArg_Parser _parser = {
+        .keywords = _keywords,
+        .fname = "depr_param",
+        .kwtuple = KWTUPLE,
+    };
+    #undef KWTUPLE
+    PyObject *argsbuf[4];
+    Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 
0;
+    PyObject *a = Py_None;
+    PyObject *b = Py_None;
+    PyObject *c = Py_None;
+    PyObject *d = Py_None;
+
+    args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+            /*minpos*/ 0, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+    if (!args) {
+        goto exit;
+    }
+    if (nargs < 1) {
+        goto skip_optional_posonly;
+    }
+    noptargs--;
+    a = args[0];
+    if (nargs < 2) {
+        goto skip_optional_posonly;
+    }
+    noptargs--;
+    if (PyErr_WarnEx(PyExc_DeprecationWarning,
+            "Passing the argument 'b' to depr_param() is deprecated. It will "
+            "be removed in Python 3.14.", 1))
+    {
+        goto exit;
+    }
+    b = args[1];
+    if (nargs < 3) {
+        goto skip_optional_posonly;
+    }
+    noptargs--;
+    if (PyErr_WarnEx(PyExc_DeprecationWarning,
+            "Passing the argument 'c' to depr_param() is deprecated. It will "
+            "be removed in Python 3.14.", 1))
+    {
+        goto exit;
+    }
+    c = args[2];
+skip_optional_posonly:
+    if (!noptargs) {
+        goto skip_optional_kwonly;
+    }
+    if (PyErr_WarnEx(PyExc_DeprecationWarning,
+            "Passing the argument 'd' to depr_param() is deprecated. It will "
+            "be removed in Python 3.14.", 1))
+    {
+        goto exit;
+    }
+    d = args[3];
+skip_optional_kwonly:
+    return_value = depr_param_impl(module, a, b, c, d);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(depr_multi__doc__,
 "depr_multi($module, a, /, b, c, d, e, f, *, g)\n"
 "--\n"
@@ -2475,4 +2681,4 @@ depr_multi(PyObject *module, PyObject *const *args, 
Py_ssize_t nargs, PyObject *
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=9429e9340f69c4b7 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=858abe8a5a885725 input=a9049054013a1b77]*/
diff --git a/Tools/clinic/libclinic/clanguage.py 
b/Tools/clinic/libclinic/clanguage.py
index 3747e752f8a7608..ff6749ad1fe1736 100644
--- a/Tools/clinic/libclinic/clanguage.py
+++ b/Tools/clinic/libclinic/clanguage.py
@@ -91,7 +91,8 @@ def compiler_deprecated_warning(
     ) -> str | None:
         minversion: VersionTuple | None = None
         for p in parameters:
-            for version in p.deprecated_positional, p.deprecated_keyword:
+            for version in (p.deprecated_positional, p.deprecated_keyword,
+                            p.deprecated_until):
                 if version and (not minversion or minversion > version):
                     minversion = version
         if not minversion:
diff --git a/Tools/clinic/libclinic/converter.py 
b/Tools/clinic/libclinic/converter.py
index 29cbad4d5a84c42..a21478678a478c1 100644
--- a/Tools/clinic/libclinic/converter.py
+++ b/Tools/clinic/libclinic/converter.py
@@ -281,11 +281,18 @@ def converter_init(self) -> None:
     def c_default_init(self) -> None:
         return
 
+    # An alternative name of a preceding parameter: they share
+    # the same C variable.
+    alias_of: Parameter | None = None
+
     def is_optional(self) -> bool:
         return (self.default is not unspecified)
 
     def _render_self(self, parameter: Parameter, data: CRenderData) -> None:
         self.parameter = parameter
+        if self.alias_of is not None:
+            # Everything is rendered for the aliased parameter.
+            return
         name = self.parser_name
 
         # impl_arguments
@@ -307,6 +314,13 @@ def _render_non_self(
         self.parameter = parameter
         name = self.name
 
+        if self.alias_of is not None:
+            # Only the keyword is new, the rest is rendered for the
+            # aliased parameter.
+            data.keywords.append(parameter.name)
+            data.format_units.append(self.format_unit)
+            return
+
         # declarations
         d = self.declaration(in_parser=True)
         data.declarations.append(d)
diff --git a/Tools/clinic/libclinic/dsl_parser.py 
b/Tools/clinic/libclinic/dsl_parser.py
index 0202d9d3daf8875..bbb7939c2e15757 100644
--- a/Tools/clinic/libclinic/dsl_parser.py
+++ b/Tools/clinic/libclinic/dsl_parser.py
@@ -251,6 +251,7 @@ class DSLParser:
     positional_only: bool
     deprecated_positional: VersionTuple | None
     deprecated_keyword: VersionTuple | None
+    deprecated_until: VersionTuple | None
     group_stack: list[int]
     group_count: int
     parameter_state: ParamState
@@ -266,6 +267,7 @@ class DSLParser:
     # Line of the file which is being parsed.
     line_number: int | None
     from_version_re = re.compile(r'([*/]) +\[from +(.+)\]')
+    until_version_re = re.compile(r'\[until +(.+?)\] +(.+)')
     permit_long_summary = False
     permit_long_docstring_body = False
 
@@ -295,6 +297,7 @@ def reset(self) -> None:
         self.positional_only = False
         self.deprecated_positional = None
         self.deprecated_keyword = None
+        self.deprecated_until = None
         self.group_stack = []
         self.group_count = 0
         self.parameter_state: ParamState = ParamState.START
@@ -922,6 +925,12 @@ def state_parameter(self, line: str) -> None:
             line = match[1]
             version = self.parse_version(match[2])
 
+        self.deprecated_until = None
+        match = self.until_version_re.fullmatch(line)
+        if match:
+            self.deprecated_until = self.parse_version(match[1], 'until')
+            line = match[2]
+
         func = self.function
         match line:
             case '*':
@@ -1171,6 +1180,7 @@ def bad_node(self, node: ast.AST) -> None:
 
         p = Parameter(parameter_name, kind, function=self.function,
                       converter=converter, default=value,
+                      deprecated_until=self.deprecated_until,
                       group=self.group_stack[-1] if self.group_stack else 0,
                       group_depth=len(self.group_stack),
                       deprecated_positional=self.deprecated_positional,
@@ -1182,6 +1192,26 @@ def bad_node(self, node: ast.AST) -> None:
         elif names and parameter_name == names[0] and c_name is None:
             fail(f"Parameter {parameter_name!r} requires a custom C name")
 
+        # A parameter which shares the C variable of a preceding parameter
+        # is an alternative name (an alias) of it.
+        for existing in self.function.parameters.values():
+            if existing.converter.name == converter.name:
+                if not self.keyword_only:
+                    fail(f"Alias {parameter_name!r} of the parameter "
+                         f"{existing.name!r} must be keyword-only.")
+                if value is unspecified:
+                    fail(f"Alias {parameter_name!r} of the parameter "
+                         f"{existing.name!r} must have a default value.")
+                converter.alias_of = existing
+                break
+
+        # A deprecated parameter is going away, so calls which do not pass
+        # it must already be valid.
+        if self.deprecated_until is not None and value is unspecified:
+            fail(f"Deprecated parameter {parameter_name!r} "
+                 f"must have a default value.")
+
+
         key = f"{parameter_name}_as_{c_name}" if c_name else parameter_name
         self.function.parameters[key] = p
 
@@ -1211,17 +1241,18 @@ def parse_converter(
                     "Annotations must be either a name, a function call, or a 
string."
                 )
 
-    def parse_version(self, thenceforth: str) -> VersionTuple:
-        """Parse Python version in `[from ...]` marker."""
+    def parse_version(self, version: str, marker: str = 'from') -> 
VersionTuple:
+        """Parse Python version in `[from ...]` or `[until ...]` marker."""
         assert isinstance(self.function, Function)
 
         try:
-            major, minor = thenceforth.split(".")
+            major, minor = version.split(".")
             return int(major), int(minor)
         except ValueError:
             fail(
-                f"Function {self.function.name!r}: expected format '[from 
major.minor]' "
-                f"where 'major' and 'minor' are integers; got {thenceforth!r}"
+                f"Function {self.function.name!r}: expected format "
+                f"'[{marker} major.minor]' where 'major' and 'minor' are "
+                f"integers; got {version!r}"
             )
 
     def parse_star(self, function: Function, version: VersionTuple | None) -> 
None:
@@ -1343,12 +1374,23 @@ def parse_slash(self, function: Function, version: 
VersionTuple | None) -> None:
             fail(f"Function {function.name!r} has an unsupported group 
configuration. "
                  f"(Unexpected state {self.parameter_state}.d)")
         # fixup preceding parameters
+        deprecated = None
         for p in function.parameters.values():
             if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
                 if version is None:
                     p.kind = inspect.Parameter.POSITIONAL_ONLY
                 elif p.deprecated_keyword is None:
                     p.deprecated_keyword = version
+            if p.kind is inspect.Parameter.POSITIONAL_ONLY:
+                # A positional-only argument can only be passed after all
+                # preceding ones, so removing a parameter would leave no
+                # way to pass those which follow it.
+                if p.deprecated_until is not None:
+                    deprecated = p
+                elif deprecated is not None:
+                    fail(f"Parameter {p.name!r} cannot follow the deprecated "
+                         f"parameter {deprecated.name!r}: only the last "
+                         f"positional-only parameters can be deprecated.")
 
     def state_parameter_docstring_start(self, line: str) -> None:
         assert self.indent.margin is not None, "self.margin.infer() has not 
yet been called to set the margin"
@@ -1503,7 +1545,10 @@ def docstring_line(index: int) -> int | None:
         lines.insert(0, '{signature}')
 
         # finalize docstring
-        params = f.render_parameters
+        # An alias is not shown in the signature: only one of the
+        # alternative names can be used in a call.
+        params = [p for p in f.render_parameters
+                  if p.converter.alias_of is None]
         parameters = self.format_docstring_parameters(params)
         signature = self.format_docstring_signature(f, params)
         docstring = "\n".join(lines)
diff --git a/Tools/clinic/libclinic/function.py 
b/Tools/clinic/libclinic/function.py
index d61cc136b7fefac..83d929fcc9207ab 100644
--- a/Tools/clinic/libclinic/function.py
+++ b/Tools/clinic/libclinic/function.py
@@ -240,6 +240,8 @@ class Parameter:
     # (`None` signifies that there is no deprecation)
     deprecated_positional: VersionTuple | None = None
     deprecated_keyword: VersionTuple | None = None
+    # The release in which the parameter will be removed.
+    deprecated_until: VersionTuple | None = None
     # Line of the file on which the parameter is declared.
     line_number: int | None = None
     right_bracket_count: int = dc.field(init=False, default=0)
diff --git a/Tools/clinic/libclinic/parse_args.py 
b/Tools/clinic/libclinic/parse_args.py
index 4aa159010e8296a..4b6b54bc4febf3a 100644
--- a/Tools/clinic/libclinic/parse_args.py
+++ b/Tools/clinic/libclinic/parse_args.py
@@ -351,6 +351,9 @@ def __init__(self, func: Function, codegen: CodeGen) -> 
None:
         self.max_pos = 0
         self.min_kw_only = 0
         for i, p in enumerate(self.parameters, 1):
+            if p.converter.alias_of is not None:
+                # An alias fills the slot of the parameter which it aliases.
+                continue
             if p.is_keyword_only():
                 assert not p.is_positional_only()
                 if not p.is_optional():
@@ -891,6 +894,8 @@ def _parse_positional_args(
                         f"Using converter {p.converter} is not supported "
                         f"in function with var-positional parameter")
                 return None
+            if p.deprecated_until is not None:
+                parsearg = self.render_deprecated(p, parsearg)
             if i >= self.min_pos:
                 # p and everything after it is optional.
                 parser_code.append(libclinic.normalize_snippet(f"""
@@ -932,8 +937,7 @@ def render_parse_all_arguments(self) -> str:
 
         Fall back to the tuple convention if the stack one cannot be used.
         """
-        for p in self.parameters:
-            p.converter.use_converter()
+        self.use_converters()
         if self.limited_capi:
             # _PyArg_ParseStack() is not part of the limited C API.
             self.fastcall = False
@@ -1045,6 +1049,71 @@ def parse_var_keyword(self) -> None:
             
parser_code.append(libclinic.normalize_snippet(self._parse_kwarg(), indent=4))
         self.parser_body(*parser_code)
 
+    def use_converters(self) -> None:
+        """Prepare for parsing all arguments by a single call.
+
+        Such call leaves nowhere to put the code checking a particular
+        argument.
+        """
+        for p in self.parameters:
+            if p.converter.alias_of is not None:
+                fail(f"Parameter {p.name!r} cannot be an alias: "
+                     f"the arguments are not parsed one by one.")
+            if p.deprecated_until is not None:
+                fail(f"Parameter {p.name!r} cannot be deprecated: "
+                     f"the arguments are not parsed one by one.")
+            p.converter.use_converter()
+
+    def render_alias(self, p: Parameter, argname_fmt: str,
+                     parsearg: str) -> str:
+        """Prepend the code checking that the alias is not in conflict.
+
+        Only one of the alternative names can be used in a call.
+        """
+        aliased = p.converter.alias_of
+        assert aliased is not None
+        i = self.parameters.index(aliased)
+        other = f"name ('{aliased.name}')"
+        arg = ''
+        if i < self.max_pos:
+            # The other name can be used for a positional argument too.
+            arg = f', {i} < nargs ? "position ({i + 1})" : "{other}"'
+            other = '%s'
+        return '\n'.join([
+            libclinic.normalize_snippet(f"""
+                if ({argname_fmt % i}) {{{{
+                    PyErr_Format(PyExc_TypeError,
+                            "argument for {self.func.name}() given by "
+                            "name ('{p.name}') and {other}"{arg});
+                    goto exit;
+                }}}}
+                """),
+            libclinic.normalize_snippet(parsearg),
+        ])
+
+    def render_deprecated(self, p: Parameter, parsearg: str) -> str:
+        """Prepend the code warning that the parameter is going away."""
+        assert p.deprecated_until is not None
+        major, minor = p.deprecated_until
+        aliased = p.converter.alias_of
+        instead = "" if aliased is None else f"Use {aliased.name!r} instead. "
+        message = (f"Passing the argument {p.name!r} to "
+                   f"{self.func.fulldisplayname}() is deprecated. {instead}"
+                   f"It will be removed in Python {major}.{minor}.")
+        code = [
+            libclinic.normalize_snippet("""
+                if (PyErr_WarnEx(PyExc_DeprecationWarning,
+                        {}, 1))
+                {{{{
+                    goto exit;
+                }}}}
+                """.format(
+                    libclinic.wrapped_c_string_literal(
+                        message, width=64, subsequent_indent=24))),
+            libclinic.normalize_snippet(parsearg),
+        ]
+        return '\n'.join(code)
+
     def parse_general(self, clang: CLanguage) -> None:
         deprecated_positionals: dict[int, Parameter] = {}
         deprecated_keywords: dict[int, Parameter] = {}
@@ -1142,6 +1211,13 @@ def parse_general(self, clang: CLanguage) -> None:
                                     "parameter (after clang)")
                 displayname = p.get_displayname(i+1)
                 parsearg = p.converter.parse_arg(argname_fmt % i, displayname, 
limited_capi=self.limited_capi)
+                if parsearg is not None:
+                    # The conflict is reported before warning about the
+                    # deprecated name which caused it.
+                    if p.deprecated_until is not None:
+                        parsearg = self.render_deprecated(p, parsearg)
+                    if p.converter.alias_of is not None:
+                        parsearg = self.render_alias(p, argname_fmt, parsearg)
                 if parsearg is None:
                     parser_code = []
                     use_parser_code = False
@@ -1198,8 +1274,7 @@ def parse_general(self, clang: CLanguage) -> None:
             if self.varpos:
                 
parser_code.append(libclinic.normalize_snippet(self._parse_vararg(), indent=4))
         else:
-            for parameter in self.parameters:
-                parameter.converter.use_converter()
+            self.use_converters()
 
             self.declarations = declare_parser(self.func, codegen=self.codegen,
                                                hasformat=True)

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to