From e505f9df297f76ad060ca4645ec9f21c30676619 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 28 Sep 2026 11:26:37 +0300 Subject: [PATCH] gh-158320: Reject optional groups with a var-positional parameter in Argument Clinic 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. --- Lib/test/test_clinic.py | 22 ++++++++ Tools/clinic/libclinic/dsl_parser.py | 9 ++++ clinic/test.c.h | 79 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 clinic/test.c.h diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 2243c43bdcbf80..62bcb97dbc45b2 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]*/