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]