https://github.com/python/cpython/commit/9e9c3c49b4a41eebb982891c3ccdc17ae5ab9963
commit: 9e9c3c49b4a41eebb982891c3ccdc17ae5ab9963
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-09-28T16:10:44+03:00
summary:
gh-158320: Reject optional groups with a var-positional parameter in Argument
Clinic (GH-158323)
Such a combination is ambiguous: the number of arguments does not tell
whether the group is passed or all arguments belong to the var-positional
parameter.
files:
A clinic/test.c.h
M Lib/test/test_clinic.py
M Tools/clinic/libclinic/dsl_parser.py
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index c3ac61d9f5c5b4..083ca8eb942066 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -2106,6 +2106,28 @@ def test_disallowed_grouping__no_matching_bracket(self):
err = "Function 'empty_group' has a ']' without a matching '['"
self.expect_failure(block, err)
+ def test_disallowed_grouping__varpos(self):
+ err = "cannot use optional groups with a var-positional parameter"
+ block = """
+ module foo
+ foo.bar
+ [
+ a: int
+ ]
+ *args: tuple
+ """
+ self.expect_failure(block, err, lineno=5)
+ block = """
+ module foo
+ foo.bar
+ a: int
+ *args: tuple
+ [
+ b: int
+ ]
+ """
+ self.expect_failure(block, err, lineno=4)
+
def test_disallowed_grouping__must_be_position_only(self):
dataset = ("""
with_kwds
diff --git a/Tools/clinic/libclinic/dsl_parser.py
b/Tools/clinic/libclinic/dsl_parser.py
index a798fac4f3fd09..a79e08ba0e437e 100644
--- a/Tools/clinic/libclinic/dsl_parser.py
+++ b/Tools/clinic/libclinic/dsl_parser.py
@@ -1161,6 +1161,12 @@ def bad_node(self, node: ast.AST) -> None:
kind: inspect._ParameterKind
if is_vararg:
+ if any(p.group for p in self.function.parameters.values()):
+ # With "foo([a, b], *args)" the number of arguments does not
+ # tell whether the group is passed or all arguments belong
+ # to the var-positional parameter.
+ fail(f"Function {self.function.name!r} cannot use optional "
+ f"groups with a var-positional parameter.")
kind = inspect.Parameter.VAR_POSITIONAL
elif is_var_keyword:
kind = inspect.Parameter.VAR_KEYWORD
@@ -1303,6 +1309,9 @@ def parse_star(self, function: Function, version:
VersionTuple | None) -> None:
def parse_opening_square_bracket(self, function: Function) -> None:
"""Parse opening parameter group symbol '['."""
+ if any(p.is_vararg() for p in function.parameters.values()):
+ fail(f"Function {function.name!r} cannot use optional groups "
+ f"with a var-positional parameter.")
# A group can only be nested in a group which does not contain
# parameters yet, but two groups on the same nesting level can
# follow each other.
diff --git a/clinic/test.c.h b/clinic/test.c.h
new file mode 100644
index 00000000000000..de583d8b0cfc2d
--- /dev/null
+++ b/clinic/test.c.h
@@ -0,0 +1,79 @@
+/*[clinic input]
+preserve
+[clinic start generated code]*/
+
+#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+# include "pycore_gc.h" // PyGC_Head
+# include "pycore_runtime.h" // _Py_ID()
+#endif
+#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
+
+PyDoc_STRVAR(foo_bar__doc__,
+"bar($module, /, *args, a)\n"
+"--\n"
+"\n");
+
+#define FOO_BAR_METHODDEF \
+ {"bar", _PyCFunction_CAST(foo_bar), METH_FASTCALL|METH_KEYWORDS,
foo_bar__doc__},
+
+static PyObject *
+foo_bar_impl(PyObject *module, PyObject *args, int a);
+
+static PyObject *
+foo_bar(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('a'), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"a", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "bar",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[1];
+ PyObject * const *fastargs;
+ PyObject *__clinic_args = NULL;
+ int a;
+
+ fastargs = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+ /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 1, /*varpos*/ 1, argsbuf);
+ if (!fastargs) {
+ goto exit;
+ }
+ a = PyLong_AsInt(fastargs[0]);
+ if (a == -1 && PyErr_Occurred()) {
+ goto exit;
+ }
+ __clinic_args = PyTuple_FromArray(args, nargs);
+ if (__clinic_args == NULL) {
+ goto exit;
+ }
+ return_value = foo_bar_impl(module, __clinic_args, a);
+
+exit:
+ /* Cleanup for args */
+ Py_XDECREF(__clinic_args);
+
+ return return_value;
+}
+/*[clinic end generated code: output=aa3dc5fffe57e996 input=a9049054013a1b77]*/
_______________________________________________
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]