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]

Reply via email to