From 414eb176fe4d4cdd3e3374e7af72650eb12a0bab Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 5 Aug 2026 09:21:21 +0300 Subject: [PATCH 1/2] gh-64502: Fix Argument Clinic support of optional groups with defaults (GH-155191) Parameters with a default value which are not in any group were always required in the generated argument parsing code, although they were rendered as optional in the signature. They can now be omitted, and ambiguous combinations of optional groups and parameters with a default value are rejected. (cherry picked from commit caac9278306d3cb96b22f51f5003e20b7a6aa18c) Co-authored-by: Serhiy Storchaka Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/clinic.test.c | 61 ++++++++++++ Lib/test/test_clinic.py | 39 ++++++++ ...6-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst | 3 + Modules/_testclinic.c | 48 ++++++++++ Modules/clinic/_testclinic.c.h | 92 ++++++++++++++++++- Tools/c-analyzer/cpython/_parser.py | 1 + Tools/clinic/libclinic/clanguage.py | 45 +++++++-- 7 files changed, 278 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index 4cec427dbaa8850..6d3c42ddbd03fed 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5641,6 +5641,67 @@ Test___init___impl(TestObj *self, PyObject *a, int group_right_1, /*[clinic end generated code: output=2bbb8ea60e8f57a6 input=10f5d0f1e8e466ef]*/ +/*[clinic input] +group_and_optional_parameter + [ + a: object + b: object + ] + c: object = None + / +The optional parameter can be omitted with or without the group. +[clinic start generated code]*/ + +PyDoc_STRVAR(group_and_optional_parameter__doc__, +"group_and_optional_parameter([a, b,] c=None)\n" +"The optional parameter can be omitted with or without the group."); + +#define GROUP_AND_OPTIONAL_PARAMETER_METHODDEF \ + {"group_and_optional_parameter", (PyCFunction)group_and_optional_parameter, METH_VARARGS, group_and_optional_parameter__doc__}, + +static PyObject * +group_and_optional_parameter_impl(PyObject *module, int group_left_1, + PyObject *a, PyObject *b, PyObject *c); + +static PyObject * +group_and_optional_parameter(PyObject *module, PyObject *args) +{ + PyObject *return_value = NULL; + int group_left_1 = 0; + PyObject *a = NULL; + PyObject *b = NULL; + PyObject *c = Py_None; + + switch (PyTuple_GET_SIZE(args)) { + case 0: + case 1: + if (!PyArg_ParseTuple(args, "|O:group_and_optional_parameter", &c)) { + goto exit; + } + break; + case 2: + case 3: + if (!PyArg_ParseTuple(args, "OO|O:group_and_optional_parameter", &a, &b, &c)) { + goto exit; + } + group_left_1 = 1; + break; + default: + PyErr_SetString(PyExc_TypeError, "group_and_optional_parameter requires 0 to 3 arguments"); + goto exit; + } + return_value = group_and_optional_parameter_impl(module, group_left_1, a, b, c); + +exit: + return return_value; +} + +static PyObject * +group_and_optional_parameter_impl(PyObject *module, int group_left_1, + PyObject *a, PyObject *b, PyObject *c) +/*[clinic end generated code: output=3faea69eafd5bbbe input=7f0fbb6124f5a972]*/ + + /*[clinic input] Test._pyarg_parsestackandkeywords cls: defining_class diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 73bb942af7c0a18..885b2be16e64c60 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -329,6 +329,24 @@ def __init__(self): """ self.expect_failure(block, err, lineno=8) + def test_ambiguous_group_and_optional_parameters(self): + err = ("Function 'my_test_func' has an ambiguous group configuration: " + "a call with 2 argument(s) can be parsed in more than one way.") + block = """ + /*[clinic input] + my_test_func + + [ + a: object + b: object + ] + c: object = None + d: object = None + / + [clinic start generated code]*/ + """ + self.expect_failure(block, err) + def test_star_after_vararg(self): err = "'my_test_func' uses '*' more than once." block = """ @@ -3685,6 +3703,27 @@ def test_varpos_kwonly_req_opt(self): self.assertEqual(fn(1, a=2, b=3), ((1,), 2, 3, False)) self.assertEqual(fn(1, a=2, b=3, c=4), ((1,), 2, 3, 4)) + def test_group_and_opt(self): + # fn([a, b,] c=None) + fn = ac_tester.group_and_opt + self.assertEqual(fn(), (False, None, None, None)) + self.assertEqual(fn(1), (False, None, None, 1)) + self.assertEqual(fn(1, 2), (True, 1, 2, None)) + self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3)) + self.assertRaises(TypeError, fn, 1, 2, 3, 4) + self.assertRaises(TypeError, fn, c=1) + + def test_group_and_two_opt(self): + # fn([a, b, c,] d=None, e=None) + fn = ac_tester.group_and_two_opt + self.assertEqual(fn(), (False, None, None, None, None, None)) + self.assertEqual(fn(1), (False, None, None, None, 1, None)) + self.assertEqual(fn(1, 2), (False, None, None, None, 1, 2)) + self.assertEqual(fn(1, 2, 3), (True, 1, 2, 3, None, None)) + self.assertEqual(fn(1, 2, 3, 4), (True, 1, 2, 3, 4, None)) + self.assertEqual(fn(1, 2, 3, 4, 5), (True, 1, 2, 3, 4, 5)) + self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5, 6) + def test_gh_32092_oob(self): ac_tester.gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6) diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst new file mode 100644 index 000000000000000..da9647d1fdd369b --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst @@ -0,0 +1,3 @@ +Fix Argument Clinic support of parameters with a default value used together +with optional groups. +Such parameters were always required in the generated parsing code. diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c index 1d23198dac52b2d..9cc526f768a23a9 100644 --- a/Modules/_testclinic.c +++ b/Modules/_testclinic.c @@ -1232,6 +1232,52 @@ posonly_poskw_varpos_array_impl(PyObject *module, PyObject *a, PyObject *b, } +/*[clinic input] +group_and_opt + + [ + a: object + b: object + ] + c: object = None + / + +[clinic start generated code]*/ + +static PyObject * +group_and_opt_impl(PyObject *module, int group_left_1, PyObject *a, + PyObject *b, PyObject *c) +/*[clinic end generated code: output=23413ec545526111 input=8a84d8f44bc8bd0b]*/ +{ + return pack_arguments_newref(4, group_left_1 ? Py_True : Py_False, + a, b, c); +} + + +/*[clinic input] +group_and_two_opt + + [ + a: object + b: object + c: object + ] + d: object = None + e: object = None + / + +[clinic start generated code]*/ + +static PyObject * +group_and_two_opt_impl(PyObject *module, int group_left_1, PyObject *a, + PyObject *b, PyObject *c, PyObject *d, PyObject *e) +/*[clinic end generated code: output=1427c4b3c35f24ff input=cdda98eec1e365ea]*/ +{ + return pack_arguments_newref(6, group_left_1 ? Py_True : Py_False, + a, b, c, d, e); +} + + /*[clinic input] gh_32092_oob @@ -2368,6 +2414,8 @@ static PyMethodDef tester_methods[] = { POSONLY_VARPOS_ARRAY_METHODDEF POSONLY_REQ_OPT_VARPOS_ARRAY_METHODDEF POSONLY_POSKW_VARPOS_ARRAY_METHODDEF + GROUP_AND_OPT_METHODDEF + GROUP_AND_TWO_OPT_METHODDEF GH_32092_OOB_METHODDEF GH_32092_KW_PASS_METHODDEF diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h index b652634892c27f0..964bdf714ef78d0 100644 --- a/Modules/clinic/_testclinic.c.h +++ b/Modules/clinic/_testclinic.c.h @@ -3358,6 +3358,96 @@ posonly_poskw_varpos_array(PyObject *module, PyObject *const *args, Py_ssize_t n return return_value; } +PyDoc_STRVAR(group_and_opt__doc__, +"group_and_opt([a, b,] c=None)"); + +#define GROUP_AND_OPT_METHODDEF \ + {"group_and_opt", (PyCFunction)group_and_opt, METH_VARARGS, group_and_opt__doc__}, + +static PyObject * +group_and_opt_impl(PyObject *module, int group_left_1, PyObject *a, + PyObject *b, PyObject *c); + +static PyObject * +group_and_opt(PyObject *module, PyObject *args) +{ + PyObject *return_value = NULL; + int group_left_1 = 0; + PyObject *a = NULL; + PyObject *b = NULL; + PyObject *c = Py_None; + + switch (PyTuple_GET_SIZE(args)) { + case 0: + case 1: + if (!PyArg_ParseTuple(args, "|O:group_and_opt", &c)) { + goto exit; + } + break; + case 2: + case 3: + if (!PyArg_ParseTuple(args, "OO|O:group_and_opt", &a, &b, &c)) { + goto exit; + } + group_left_1 = 1; + break; + default: + PyErr_SetString(PyExc_TypeError, "group_and_opt requires 0 to 3 arguments"); + goto exit; + } + return_value = group_and_opt_impl(module, group_left_1, a, b, c); + +exit: + return return_value; +} + +PyDoc_STRVAR(group_and_two_opt__doc__, +"group_and_two_opt([a, b, c,] d=None, e=None)"); + +#define GROUP_AND_TWO_OPT_METHODDEF \ + {"group_and_two_opt", (PyCFunction)group_and_two_opt, METH_VARARGS, group_and_two_opt__doc__}, + +static PyObject * +group_and_two_opt_impl(PyObject *module, int group_left_1, PyObject *a, + PyObject *b, PyObject *c, PyObject *d, PyObject *e); + +static PyObject * +group_and_two_opt(PyObject *module, PyObject *args) +{ + PyObject *return_value = NULL; + int group_left_1 = 0; + PyObject *a = NULL; + PyObject *b = NULL; + PyObject *c = NULL; + PyObject *d = Py_None; + PyObject *e = Py_None; + + switch (PyTuple_GET_SIZE(args)) { + case 0: + case 1: + case 2: + if (!PyArg_ParseTuple(args, "|OO:group_and_two_opt", &d, &e)) { + goto exit; + } + break; + case 3: + case 4: + case 5: + if (!PyArg_ParseTuple(args, "OOO|OO:group_and_two_opt", &a, &b, &c, &d, &e)) { + goto exit; + } + group_left_1 = 1; + break; + default: + PyErr_SetString(PyExc_TypeError, "group_and_two_opt requires 0 to 5 arguments"); + goto exit; + } + return_value = group_and_two_opt_impl(module, group_left_1, a, b, c, d, e); + +exit: + return return_value; +} + PyDoc_STRVAR(gh_32092_oob__doc__, "gh_32092_oob($module, /, pos1, pos2, *varargs, kw1=None, kw2=None)\n" "--\n" @@ -4481,4 +4571,4 @@ _testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO exit: return return_value; } -/*[clinic end generated code: output=8af194d826d6740d input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c92c0125c28434c0 input=a9049054013a1b77]*/ diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index f5dcd5c76c55f24..5803b6f0667c45f 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -309,6 +309,7 @@ def format_tsv_lines(lines): _abs('Modules/posixmodule.c'): (20_000, 500), _abs('Modules/termios.c'): (10_000, 800), _abs('Modules/_testcapimodule.c'): (20_000, 400), + _abs('Modules/_testclinic.c'): (20_000, 400), _abs('Modules/expat/expat.h'): (10_000, 400), _abs('Objects/stringlib/unicode_format.h'): (10_000, 400), _abs('Objects/typeobject.c'): (35_000, 200), diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index 7f02c7790f015aa..a76fddb7602001e 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -1,6 +1,5 @@ from __future__ import annotations import itertools -import sys import textwrap from typing import TYPE_CHECKING, Literal, Final from operator import attrgetter @@ -12,7 +11,7 @@ from libclinic.codegen import CRenderData, TemplateDict, CodeGen from libclinic.language import Language from libclinic.function import ( - Module, Class, Function, Parameter, + Module, Class, Function, Parameter, ParamTuple, permute_optional_groups, GETTER, SETTER, METHOD_INIT) from libclinic.converters import self_converter @@ -21,6 +20,20 @@ from libclinic.app import Clinic +def count_required(subset: ParamTuple) -> int: + """Return the number of arguments which cannot be omitted. + + A parameter in an optional group is passed together with its group, + so only trailing parameters with a default value can be omitted. + """ + count = len(subset) + for p in reversed(subset): + if p.group or not p.is_optional(): + break + count -= 1 + return count + + def c_id(name: str) -> str: if len(name) == 1 and ord(name) < 256: if name.isalnum(): @@ -301,18 +314,26 @@ def render_option_group_parsing( assert group is not None group.append(p) - count_min = sys.maxsize - count_max = -1 + # Map the number of arguments to the subset which accepts it. + subsets: dict[int, ParamTuple] = {} + for subset in permute_optional_groups(left, required, right): + for count in range(count_required(subset), len(subset) + 1): + if count in subsets: + fail(f"Function {f.full_name!r} has an ambiguous group " + f"configuration: a call with {count} argument(s) " + f"can be parsed in more than one way.") + subsets[count] = subset if limited_capi: nargs = 'PyTuple_Size(args)' else: nargs = 'PyTuple_GET_SIZE(args)' out.append(f"switch ({nargs}) {{\n") - for subset in permute_optional_groups(left, required, right): - count = len(subset) - count_min = min(count_min, count) - count_max = max(count_max, count) + for count, subset in sorted(subsets.items()): + if count < len(subset): + # The omitted parameters are parsed by the following case. + out.append(f" case {count}:\n") + continue if count == 0: out.append(""" case 0: @@ -324,7 +345,11 @@ def render_option_group_parsing( d: dict[str, str | int] = {} d['count'] = count d['name'] = f.name - d['format_units'] = "".join(p.converter.format_unit for p in subset) + format_units = [p.converter.format_unit for p in subset] + n_required = count_required(subset) + if n_required < count: + format_units.insert(n_required, '|') + d['format_units'] = "".join(format_units) parse_arguments: list[str] = [] for p in subset: @@ -351,7 +376,7 @@ def render_option_group_parsing( out.append(" default:\n") s = ' PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n' - out.append(s.format(f.full_name, count_min, count_max)) + out.append(s.format(f.full_name, min(subsets), max(subsets))) out.append(' goto exit;\n') out.append("}") From 78b447a89c92462e5fd0fee41c5f3bc8b0c0c3d2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 5 Aug 2026 15:04:11 +0300 Subject: [PATCH 2/2] gh-64502: Increase the c-analyzer size limit for the _testclinic header The added test functions made Modules/clinic/_testclinic.c.h exceed the limit. The main and 3.15 branches already use the larger value. Co-authored-by: Serhiy Storchaka Co-Authored-By: Claude Opus 5 (1M context) --- Tools/c-analyzer/cpython/_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index 5803b6f0667c45f..ec9406f051fd958 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -334,7 +334,7 @@ def format_tsv_lines(lines): _abs('Modules/_ssl_data_300.h'): (80_000, 10_000), _abs('Modules/_ssl_data_111.h'): (80_000, 10_000), _abs('Modules/cjkcodecs/mappings_*.h'): (160_000, 2_000), - _abs('Modules/clinic/_testclinic.c.h'): (120_000, 5_000), + _abs('Modules/clinic/_testclinic.c.h'): (125_000, 5_000), _abs('Modules/unicodedata_db.h'): (180_000, 3_000), _abs('Modules/unicodename_db.h'): (1_200_000, 15_000), _abs('Objects/unicodetype_db.h'): (240_000, 3_000),