https://github.com/python/cpython/commit/80c651979093b020d94dc5ba6bf90c6a19694a56
commit: 80c651979093b020d94dc5ba6bf90c6a19694a56
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-05T14:39:17+03:00
summary:

[3.13] gh-64502: Fix Argument Clinic support of optional groups with defaults 
(GH-155191) (GH-155217)

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: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-04-17-12-33.gh-issue-64502.Qv7mLp.rst
M Lib/test/clinic.test.c
M Lib/test/test_clinic.py
M Modules/_testclinic.c
M Modules/clinic/_testclinic.c.h
M Tools/c-analyzer/cpython/_parser.py
M Tools/clinic/libclinic/clanguage.py

diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c
index bfc15c024de7fde..7e0761f12497cff 100644
--- a/Lib/test/clinic.test.c
+++ b/Lib/test/clinic.test.c
@@ -5337,6 +5337,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 d277ac0a636aa4c..feb1e2b25d59c35 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -324,6 +324,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 = """
@@ -3598,6 +3616,27 @@ def test_vararg_kwonly_req_opt(self):
         self.assertEqual(fn(1, a=2, b=3), ((1,), 2, 3, None))
         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 c352710cf4558d0..32d2bcff9f449bd 100644
--- a/Modules/_testclinic.c
+++ b/Modules/_testclinic.c
@@ -1090,6 +1090,52 @@ vararg_kwonly_req_opt_impl(PyObject *module, PyObject 
*args, PyObject *a,
 
 
 
+/*[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
 
@@ -1949,6 +1995,8 @@ static PyMethodDef tester_methods[] = {
     VARARG_WITH_DEFAULT2_METHODDEF
     VARARG_WITH_ONLY_DEFAULTS_METHODDEF
     VARARG_KWONLY_REQ_OPT_METHODDEF
+    GROUP_AND_OPT_METHODDEF
+    GROUP_AND_TWO_OPT_METHODDEF
     GH_32092_OOB_METHODDEF
     GH_32092_KW_PASS_METHODDEF
     GH_99233_REFCOUNT_METHODDEF
diff --git a/Modules/clinic/_testclinic.c.h b/Modules/clinic/_testclinic.c.h
index 63cef8aac43db6a..c249bba13cbcd09 100644
--- a/Modules/clinic/_testclinic.c.h
+++ b/Modules/clinic/_testclinic.c.h
@@ -2771,6 +2771,96 @@ vararg_kwonly_req_opt(PyObject *module, PyObject *const 
*args, Py_ssize_t nargs,
     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"
@@ -3385,4 +3475,4 @@ _testclinic_TestClass_get_defining_class_arg(PyObject 
*self, PyTypeObject *cls,
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=c556818496a3ec72 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=eafedd2a830669f5 input=a9049054013a1b77]*/
diff --git a/Tools/c-analyzer/cpython/_parser.py 
b/Tools/c-analyzer/cpython/_parser.py
index 12010f0e9c05491..28dd5fe9ff88aff 100644
--- a/Tools/c-analyzer/cpython/_parser.py
+++ b/Tools/c-analyzer/cpython/_parser.py
@@ -317,6 +317,7 @@ def clean_lines(text):
     _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 4c5357f098cb98f..165ab40621fc6ce 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:
@@ -326,7 +347,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:
@@ -353,7 +378,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("}")
 

_______________________________________________
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