https://github.com/python/cpython/commit/7763c983c9b9e0c44059746b2e6b0d53a8af0815
commit: 7763c983c9b9e0c44059746b2e6b0d53a8af0815
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-05T09:38:40Z
summary:

gh-64502: Support several optional groups on the same level in Argument Clinic 
(GH-155210)

Groups on the same nesting level, like in "[y, x,] [n,] attr", can now be
omitted independently of each other.  A group is now identified by a unique
number instead of its nesting level.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>

files:
A Misc/NEWS.d/next/Tools-Demos/2026-08-04-19-05-41.gh-issue-64502.Rb2wKt.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/clinic/libclinic/clanguage.py
M Tools/clinic/libclinic/dsl_parser.py
M Tools/clinic/libclinic/function.py

diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c
index 870e4f6956ce4f..146f57a2a11342 100644
--- a/Lib/test/clinic.test.c
+++ b/Lib/test/clinic.test.c
@@ -5830,6 +5830,85 @@ group_and_optional_parameter_impl(PyObject *module, int 
group_left_1,
 /*[clinic end generated code: output=3faea69eafd5bbbe input=7f0fbb6124f5a972]*/
 
 
+/*[clinic input]
+two_groups_on_the_same_level
+    [
+    a: object
+    b: object
+    ]
+    [
+    c: object
+    ]
+    d: object
+    /
+Groups on the same level are independent of each other.
+[clinic start generated code]*/
+
+PyDoc_STRVAR(two_groups_on_the_same_level__doc__,
+"two_groups_on_the_same_level([a, b,] [c,] d)\n"
+"Groups on the same level are independent of each other.");
+
+#define TWO_GROUPS_ON_THE_SAME_LEVEL_METHODDEF    \
+    {"two_groups_on_the_same_level", 
(PyCFunction)two_groups_on_the_same_level, METH_VARARGS, 
two_groups_on_the_same_level__doc__},
+
+static PyObject *
+two_groups_on_the_same_level_impl(PyObject *module, int group_left_1,
+                                  PyObject *a, PyObject *b, int group_left_2,
+                                  PyObject *c, PyObject *d);
+
+static PyObject *
+two_groups_on_the_same_level(PyObject *module, PyObject *args)
+{
+    PyObject *return_value = NULL;
+    int group_left_1 = 0;
+    PyObject *a = NULL;
+    PyObject *b = NULL;
+    int group_left_2 = 0;
+    PyObject *c = NULL;
+    PyObject *d;
+
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:two_groups_on_the_same_level", &d)) 
{
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "OO:two_groups_on_the_same_level", &c, 
&d)) {
+                goto exit;
+            }
+            group_left_2 = 1;
+            break;
+        case 3:
+            if (!PyArg_ParseTuple(args, "OOO:two_groups_on_the_same_level", 
&a, &b, &d)) {
+                goto exit;
+            }
+            group_left_1 = 1;
+            break;
+        case 4:
+            if (!PyArg_ParseTuple(args, "OOOO:two_groups_on_the_same_level", 
&a, &b, &c, &d)) {
+                goto exit;
+            }
+            group_left_1 = 1;
+            group_left_2 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "two_groups_on_the_same_level 
requires 1 to 4 arguments");
+            goto exit;
+    }
+    return_value = two_groups_on_the_same_level_impl(module, group_left_1, a, 
b, group_left_2, c, d);
+
+exit:
+    return return_value;
+}
+
+static PyObject *
+two_groups_on_the_same_level_impl(PyObject *module, int group_left_1,
+                                  PyObject *a, PyObject *b, int group_left_2,
+                                  PyObject *c, PyObject *d)
+/*[clinic end generated code: output=508a61ee582da21e input=1b45d9b675b32d1a]*/
+
+
 /*[clinic input]
 Test._pyarg_parsestackandkeywords
     cls: defining_class
diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py
index f35b11cff551a8..cb4507dcac2336 100644
--- a/Lib/test/test_clinic.py
+++ b/Lib/test/test_clinic.py
@@ -836,7 +836,7 @@ def _test(self, l, m, r, output):
         self.assertEqual(output, computed)
 
     def test_range(self):
-        self._test([['start']], ['stop'], [['step']],
+        self._test([[['start']]], ['stop'], [[['step']]],
           (
             ('stop',),
             ('start', 'stop',),
@@ -844,7 +844,7 @@ def test_range(self):
           ))
 
     def test_add_window(self):
-        self._test([['x', 'y']], ['ch'], [['attr']],
+        self._test([[['x', 'y']]], ['ch'], [[['attr']]],
           (
             ('ch',),
             ('ch', 'attr'),
@@ -853,7 +853,8 @@ def test_add_window(self):
           ))
 
     def test_ludicrous(self):
-        self._test([['a1', 'a2', 'a3'], ['b1', 'b2']], ['c1'], [['d1', 'd2'], 
['e1', 'e2', 'e3']],
+        self._test([[['a1', 'a2', 'a3'], ['b1', 'b2']]], ['c1'],
+                   [[['d1', 'd2'], ['e1', 'e2', 'e3']]],
           (
           ('c1',),
           ('b1', 'b2', 'c1'),
@@ -864,7 +865,7 @@ def test_ludicrous(self):
           ))
 
     def test_right_only(self):
-        self._test([], [], [['a'],['b'],['c']],
+        self._test([], [], [[['a'],['b'],['c']]],
           (
           (),
           ('a',),
@@ -872,9 +873,28 @@ def test_right_only(self):
           ('a', 'b', 'c')
           ))
 
+    def test_chgat(self):
+        # Two independent groups on the left.
+        self._test([[['y', 'x']], [['n']]], ['attr'], [],
+          (
+          ('attr',),
+          ('n', 'attr'),
+          ('y', 'x', 'attr'),
+          ('y', 'x', 'n', 'attr'),
+          ))
+
+    def test_independent_groups_on_the_right(self):
+        self._test([], ['a'], [[['b']], [['c', 'd']]],
+          (
+          ('a',),
+          ('a', 'b'),
+          ('a', 'c', 'd'),
+          ('a', 'b', 'c', 'd'),
+          ))
+
     def test_have_left_options_but_required_is_empty(self):
         def fn():
-            permute_optional_groups(['a'], [], [])
+            permute_optional_groups([[['a']]], [], [])
         self.assertRaises(ValueError, fn)
 
 
@@ -1715,41 +1735,74 @@ def test_nested_groups(self):
                 Attributes for the character.
         """)
 
-    def test_disallowed_grouping__two_top_groups_on_left(self):
-        err = (
-            "Function 'two_top_groups_on_left' has an unsupported group "
-            "configuration. (Unexpected state 2.b)"
-        )
-        block = """
-            module foo
-            foo.two_top_groups_on_left
+    def test_two_top_groups_on_left(self):
+        function = self.parse_function("""
+            module curses
+            curses.chgat
                 [
-                group1 : int
+                y: int
+                    Y-coordinate.
+                x: int
+                    X-coordinate.
                 ]
                 [
-                group2 : int
+                num: int
+                    Number of characters.
                 ]
-                param: int
-        """
-        self.expect_failure(block, err, lineno=5)
+                attr: long
+                    Attributes for the characters.
+                /
+        """)
+        dataset = (
+            ('y', -1), ('x', -1),
+            ('num', -2),
+            ('attr', 0),
+        )
+        for name, group in dataset:
+            with self.subTest(name=name, group=group):
+                p = function.parameters[name]
+                self.assertEqual(p.group, group)
+                self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
+        self.checkDocstring(function, """
+            chgat([y, x,] [num,] attr)
 
-    def test_disallowed_grouping__two_top_groups_on_right(self):
-        block = """
+
+              y
+                Y-coordinate.
+              x
+                X-coordinate.
+              num
+                Number of characters.
+              attr
+                Attributes for the characters.
+        """)
+
+    def test_two_top_groups_on_right(self):
+        function = self.parse_function("""
             module foo
             foo.two_top_groups_on_right
                 param: int
                 [
-                group1 : int
+                group1: int
                 ]
                 [
-                group2 : int
+                group2: int
                 ]
-        """
-        err = (
-            "Function 'two_top_groups_on_right' has an unsupported group "
-            "configuration. (Unexpected state 6.b)"
+                /
+        """)
+        dataset = (
+            ('param', 0),
+            ('group1', 1),
+            ('group2', 2),
         )
-        self.expect_failure(block, err)
+        for name, group in dataset:
+            with self.subTest(name=name, group=group):
+                p = function.parameters[name]
+                self.assertEqual(p.group, group)
+                self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
+        self.checkDocstring(function, """
+            two_top_groups_on_right(param, [group1,] [group2])
+        """)
 
     def test_disallowed_grouping__parameter_after_group_on_right(self):
         block = """
@@ -4103,6 +4156,26 @@ def test_group_and_two_opt(self):
         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_two_groups_on_left(self):
+        # fn([a, b,] [c,] d)
+        fn = ac_tester.two_groups_on_left
+        self.assertRaises(TypeError, fn)
+        self.assertEqual(fn(1), (False, None, None, False, None, 1))
+        self.assertEqual(fn(1, 2), (False, None, None, True, 1, 2))
+        self.assertEqual(fn(1, 2, 3), (True, 1, 2, False, None, 3))
+        self.assertEqual(fn(1, 2, 3, 4), (True, 1, 2, True, 3, 4))
+        self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5)
+
+    def test_two_groups_on_right(self):
+        # fn(a, [b,] [c, d])
+        fn = ac_tester.two_groups_on_right
+        self.assertRaises(TypeError, fn)
+        self.assertEqual(fn(1), (1, False, None, False, None, None))
+        self.assertEqual(fn(1, 2), (1, True, 2, False, None, None))
+        self.assertEqual(fn(1, 2, 3), (1, False, None, True, 2, 3))
+        self.assertEqual(fn(1, 2, 3, 4), (1, True, 2, True, 3, 4))
+        self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5)
+
     def test_gh_32092_oob(self):
         ac_tester.gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)
 
@@ -4705,21 +4778,21 @@ def test_permute_optional_groups(self):
             "expected": ((),),
         }
         noleft1 = {
-            "left": (), "required": ("b",), "right": ("c",),
+            "left": (), "required": ("b",), "right": (("c",),),
             "expected": (
                 ("b",),
                 ("b", "c"),
             ),
         }
         noleft2 = {
-            "left": (), "required": ("b", "c",), "right": ("d",),
+            "left": (), "required": ("b", "c",), "right": (("d",),),
             "expected": (
                 ("b", "c"),
                 ("b", "c", "d"),
             ),
         }
         noleft3 = {
-            "left": (), "required": ("b", "c",), "right": ("d", "e"),
+            "left": (), "required": ("b", "c",), "right": (("d", "e"),),
             "expected": (
                 ("b", "c"),
                 ("b", "c", "d"),
@@ -4727,21 +4800,21 @@ def test_permute_optional_groups(self):
             ),
         }
         noright1 = {
-            "left": ("a",), "required": ("b",), "right": (),
+            "left": (("a",),), "required": ("b",), "right": (),
             "expected": (
                 ("b",),
                 ("a", "b"),
             ),
         }
         noright2 = {
-            "left": ("a",), "required": ("b", "c"), "right": (),
+            "left": (("a",),), "required": ("b", "c"), "right": (),
             "expected": (
                 ("b", "c"),
                 ("a", "b", "c"),
             ),
         }
         noright3 = {
-            "left": ("a", "b"), "required": ("c",), "right": (),
+            "left": (("a", "b"),), "required": ("c",), "right": (),
             "expected": (
                 ("c",),
                 ("b", "c"),
@@ -4749,7 +4822,7 @@ def test_permute_optional_groups(self):
             ),
         }
         leftandright1 = {
-            "left": ("a",), "required": ("b",), "right": ("c",),
+            "left": (("a",),), "required": ("b",), "right": (("c",),),
             "expected": (
                 ("b",),
                 ("a", "b"),  # Prefer left.
@@ -4757,7 +4830,7 @@ def test_permute_optional_groups(self):
             ),
         }
         leftandright2 = {
-            "left": ("a", "b"), "required": ("c", "d"), "right": ("e", "f"),
+            "left": (("a", "b"),), "required": ("c", "d"), "right": (("e", 
"f"),),
             "expected": (
                 ("c", "d"),
                 ("b", "c", "d"),       # Prefer left.
@@ -4766,11 +4839,28 @@ def test_permute_optional_groups(self):
                 ("a", "b", "c", "d", "e", "f"),
             ),
         }
+        independentleft = {
+            "left": (("a",), ("b",)), "required": ("c",), "right": (),
+            "expected": (
+                ("c",),
+                ("b", "c"),
+                ("a", "b", "c"),
+            ),
+        }
+        independentright = {
+            "left": (), "required": ("a",), "right": (("b",), ("c",)),
+            "expected": (
+                ("a",),
+                ("a", "b"),
+                ("a", "b", "c"),
+            ),
+        }
         dataset = (
             empty,
             noleft1, noleft2, noleft3,
             noright1, noright2, noright3,
             leftandright1, leftandright2,
+            independentleft, independentright,
         )
         for params in dataset:
             with self.subTest(**params):
diff --git 
a/Misc/NEWS.d/next/Tools-Demos/2026-08-04-19-05-41.gh-issue-64502.Rb2wKt.rst 
b/Misc/NEWS.d/next/Tools-Demos/2026-08-04-19-05-41.gh-issue-64502.Rb2wKt.rst
new file mode 100644
index 00000000000000..d3e75435df9182
--- /dev/null
+++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-04-19-05-41.gh-issue-64502.Rb2wKt.rst
@@ -0,0 +1,3 @@
+Argument Clinic now supports several optional groups on the same nesting
+level, like in ``[y, x,] [n,] attr``.
+Such groups can be omitted independently of each other.
diff --git a/Modules/_testclinic.c b/Modules/_testclinic.c
index 5742dd65f6742a..c53bf4a0875358 100644
--- a/Modules/_testclinic.c
+++ b/Modules/_testclinic.c
@@ -1259,6 +1259,58 @@ group_and_opt_impl(PyObject *module, int group_left_1, 
PyObject *a,
 }
 
 
+/*[clinic input]
+two_groups_on_left
+
+    [
+    a: object
+    b: object
+    ]
+    [
+    c: object
+    ]
+    d: object
+    /
+
+[clinic start generated code]*/
+
+static PyObject *
+two_groups_on_left_impl(PyObject *module, int group_left_1, PyObject *a,
+                        PyObject *b, int group_left_2, PyObject *c,
+                        PyObject *d)
+/*[clinic end generated code: output=3a43d6542864e3d8 input=79fc792669696ac7]*/
+{
+    return pack_arguments_newref(6, group_left_1 ? Py_True : Py_False, a, b,
+                                 group_left_2 ? Py_True : Py_False, c, d);
+}
+
+
+/*[clinic input]
+two_groups_on_right
+
+    a: object
+    [
+    b: object
+    ]
+    [
+    c: object
+    d: object
+    ]
+    /
+
+[clinic start generated code]*/
+
+static PyObject *
+two_groups_on_right_impl(PyObject *module, PyObject *a, int group_right_1,
+                         PyObject *b, int group_right_2, PyObject *c,
+                         PyObject *d)
+/*[clinic end generated code: output=045f60f127c6e448 input=96895285f29bb501]*/
+{
+    return pack_arguments_newref(6, a, group_right_1 ? Py_True : Py_False, b,
+                                 group_right_2 ? Py_True : Py_False, c, d);
+}
+
+
 /*[clinic input]
 group_and_two_opt
 
@@ -2503,6 +2555,8 @@ static PyMethodDef tester_methods[] = {
     POSONLY_POSKW_VARPOS_ARRAY_METHODDEF
     GROUP_AND_OPT_METHODDEF
     GROUP_AND_TWO_OPT_METHODDEF
+    TWO_GROUPS_ON_LEFT_METHODDEF
+    TWO_GROUPS_ON_RIGHT_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 19b215b0cca889..3fe32d704f0140 100644
--- a/Modules/clinic/_testclinic.c.h
+++ b/Modules/clinic/_testclinic.c.h
@@ -3520,6 +3520,120 @@ group_and_opt(PyObject *module, PyObject *args)
     return return_value;
 }
 
+PyDoc_STRVAR(two_groups_on_left__doc__,
+"two_groups_on_left([a, b,] [c,] d)");
+
+#define TWO_GROUPS_ON_LEFT_METHODDEF    \
+    {"two_groups_on_left", (PyCFunction)two_groups_on_left, METH_VARARGS, 
two_groups_on_left__doc__},
+
+static PyObject *
+two_groups_on_left_impl(PyObject *module, int group_left_1, PyObject *a,
+                        PyObject *b, int group_left_2, PyObject *c,
+                        PyObject *d);
+
+static PyObject *
+two_groups_on_left(PyObject *module, PyObject *args)
+{
+    PyObject *return_value = NULL;
+    int group_left_1 = 0;
+    PyObject *a = NULL;
+    PyObject *b = NULL;
+    int group_left_2 = 0;
+    PyObject *c = NULL;
+    PyObject *d;
+
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:two_groups_on_left", &d)) {
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "OO:two_groups_on_left", &c, &d)) {
+                goto exit;
+            }
+            group_left_2 = 1;
+            break;
+        case 3:
+            if (!PyArg_ParseTuple(args, "OOO:two_groups_on_left", &a, &b, &d)) 
{
+                goto exit;
+            }
+            group_left_1 = 1;
+            break;
+        case 4:
+            if (!PyArg_ParseTuple(args, "OOOO:two_groups_on_left", &a, &b, &c, 
&d)) {
+                goto exit;
+            }
+            group_left_1 = 1;
+            group_left_2 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "two_groups_on_left requires 1 to 
4 arguments");
+            goto exit;
+    }
+    return_value = two_groups_on_left_impl(module, group_left_1, a, b, 
group_left_2, c, d);
+
+exit:
+    return return_value;
+}
+
+PyDoc_STRVAR(two_groups_on_right__doc__,
+"two_groups_on_right(a, [b,] [c, d])");
+
+#define TWO_GROUPS_ON_RIGHT_METHODDEF    \
+    {"two_groups_on_right", (PyCFunction)two_groups_on_right, METH_VARARGS, 
two_groups_on_right__doc__},
+
+static PyObject *
+two_groups_on_right_impl(PyObject *module, PyObject *a, int group_right_1,
+                         PyObject *b, int group_right_2, PyObject *c,
+                         PyObject *d);
+
+static PyObject *
+two_groups_on_right(PyObject *module, PyObject *args)
+{
+    PyObject *return_value = NULL;
+    PyObject *a;
+    int group_right_1 = 0;
+    PyObject *b = NULL;
+    int group_right_2 = 0;
+    PyObject *c = NULL;
+    PyObject *d = NULL;
+
+    switch (PyTuple_GET_SIZE(args)) {
+        case 1:
+            if (!PyArg_ParseTuple(args, "O:two_groups_on_right", &a)) {
+                goto exit;
+            }
+            break;
+        case 2:
+            if (!PyArg_ParseTuple(args, "OO:two_groups_on_right", &a, &b)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            break;
+        case 3:
+            if (!PyArg_ParseTuple(args, "OOO:two_groups_on_right", &a, &c, 
&d)) {
+                goto exit;
+            }
+            group_right_2 = 1;
+            break;
+        case 4:
+            if (!PyArg_ParseTuple(args, "OOOO:two_groups_on_right", &a, &b, 
&c, &d)) {
+                goto exit;
+            }
+            group_right_1 = 1;
+            group_right_2 = 1;
+            break;
+        default:
+            PyErr_SetString(PyExc_TypeError, "two_groups_on_right requires 1 
to 4 arguments");
+            goto exit;
+    }
+    return_value = two_groups_on_right_impl(module, a, group_right_1, b, 
group_right_2, c, d);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(group_and_two_opt__doc__,
 "group_and_two_opt([a, b, c,] d=None, e=None)");
 
@@ -4690,4 +4804,4 @@ 
_testclinic_TestClass_posonly_poskw_varpos_array_no_fastcall(PyObject *type, PyO
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=f6a3b617130c4e3a input=a9049054013a1b77]*/
+/*[clinic end generated code: output=d9d4091b2f2ed359 input=a9049054013a1b77]*/
diff --git a/Tools/clinic/libclinic/clanguage.py 
b/Tools/clinic/libclinic/clanguage.py
index 3ee06307441fac..1581a19a4fd78a 100644
--- a/Tools/clinic/libclinic/clanguage.py
+++ b/Tools/clinic/libclinic/clanguage.py
@@ -288,29 +288,44 @@ def render_option_group_parsing(
         # What if the number of arguments leads us to an ambiguous result?
         # Clinic prefers groups on the left.  So in the above example,
         # five arguments would map to B+C, not C+D.
+        #
+        # A nested group can only be omitted together with the group
+        # containing it, but groups on the same level, like G and H in
+        #
+        # [ G1 G2 ] [ H1 ] I1 I2
+        #
+        # can be omitted independently of each other.
 
         out = []
         parameters = list(f.parameters.values())
         if isinstance(parameters[0].converter, self_converter):
             del parameters[0]
 
+        # Groups are collected into chains of nested groups.  A group which
+        # is not nested in the preceding one starts a new chain.
         group: list[Parameter] | None = None
-        left = []
-        right = []
+        left: list[list[list[Parameter]]] = []
+        right: list[list[list[Parameter]]] = []
         required: list[Parameter] = []
         last: int | Literal[Sentinels.unspecified] = unspecified
+        last_depth = 0
 
         for p in parameters:
             group_id = p.group
             if group_id != last:
                 last = group_id
                 group = []
-                if group_id < 0:
-                    left.append(group)
-                elif group_id == 0:
+                if group_id == 0:
                     group = required
                 else:
-                    right.append(group)
+                    chains = left if group_id < 0 else right
+                    nested = ((p.group_depth < last_depth) if group_id < 0
+                              else (p.group_depth > last_depth))
+                    if chains and nested:
+                        chains[-1].append(group)
+                    else:
+                        chains.append([group])
+                last_depth = p.group_depth
             assert group is not None
             group.append(p)
 
diff --git a/Tools/clinic/libclinic/dsl_parser.py 
b/Tools/clinic/libclinic/dsl_parser.py
index 90e2e0d3d9c928..4dcbc815cc6f25 100644
--- a/Tools/clinic/libclinic/dsl_parser.py
+++ b/Tools/clinic/libclinic/dsl_parser.py
@@ -112,8 +112,8 @@
 class ParamState(enum.IntEnum):
     """Parameter parsing state.
 
-     [ [ a, b, ] c, ] d, e, f=3, [ g, h, [ i ] ]   <- line
-    01   2          3       4    5           6     <- state transitions
+     [ [ a, b, ] c, ] [ d, ] e, f=3, [ g, h, [ i ] ] [ j ]   <- line
+    01   2          3 12   3     4   5           6   5     6 <- state 
transitions
     """
     # Before we've seen anything.
     # Legal transitions: to LEFT_SQUARE_BEFORE or REQUIRED
@@ -251,7 +251,8 @@ class DSLParser:
     positional_only: bool
     deprecated_positional: VersionTuple | None
     deprecated_keyword: VersionTuple | None
-    group: int
+    group_stack: list[int]
+    group_count: int
     parameter_state: ParamState
     indent: IndentStack
     kind: FunctionKind
@@ -291,7 +292,8 @@ def reset(self) -> None:
         self.positional_only = False
         self.deprecated_positional = None
         self.deprecated_keyword = None
-        self.group = 0
+        self.group_stack = []
+        self.group_count = 0
         self.parameter_state: ParamState = ParamState.START
         self.indent = IndentStack()
         self.kind = CALLABLE
@@ -829,6 +831,7 @@ def to_required(self) -> None:
             assert self.function is not None
             for p in self.function.parameters.values():
                 p.group = -p.group
+            self.group_count = 0
 
     def state_parameter(self, line: str) -> None:
         assert isinstance(self.function, Function)
@@ -888,7 +891,7 @@ def parse_parameter(self, line: str) -> None:
             case ParamState.LEFT_SQUARE_BEFORE:
                 self.parameter_state = ParamState.GROUP_BEFORE
             case ParamState.GROUP_BEFORE:
-                if not self.group:
+                if not self.group_stack:
                     self.to_required()
             case ParamState.GROUP_AFTER | ParamState.OPTIONAL:
                 pass
@@ -1082,7 +1085,7 @@ def bad_node(self, node: ast.AST) -> None:
 
         if isinstance(converter, self_converter):
             if len(self.function.parameters) == 1:
-                if self.group:
+                if self.group_stack:
                     fail("A 'self' parameter cannot be in an optional group.")
                 assert self.parameter_state is ParamState.REQUIRED
                 assert value is unspecified
@@ -1096,7 +1099,7 @@ def bad_node(self, node: ast.AST) -> None:
         if isinstance(converter, defining_class_converter):
             _lp = len(self.function.parameters)
             if _lp == 1:
-                if self.group:
+                if self.group_stack:
                     fail("A 'defining_class' parameter cannot be in an 
optional group.")
                 if self.function.cls is None:
                     fail("A 'defining_class' parameter cannot be defined at 
module level.")
@@ -1110,7 +1113,9 @@ def bad_node(self, node: ast.AST) -> None:
 
 
         p = Parameter(parameter_name, kind, function=self.function,
-                      converter=converter, default=value, group=self.group,
+                      converter=converter, default=value,
+                      group=self.group_stack[-1] if self.group_stack else 0,
+                      group_depth=len(self.group_stack),
                       deprecated_positional=self.deprecated_positional)
 
         names = [k.name for k in self.function.parameters.values()]
@@ -1189,26 +1194,34 @@ def parse_star(self, function: Function, version: 
VersionTuple | None) -> None:
 
     def parse_opening_square_bracket(self, function: Function) -> None:
         """Parse opening parameter group symbol '['."""
+        # 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.
         match self.parameter_state:
             case ParamState.START | ParamState.LEFT_SQUARE_BEFORE:
                 self.parameter_state = ParamState.LEFT_SQUARE_BEFORE
+            case ParamState.GROUP_BEFORE if not self.group_stack:
+                self.parameter_state = ParamState.LEFT_SQUARE_BEFORE
             case ParamState.REQUIRED | ParamState.GROUP_AFTER:
                 self.parameter_state = ParamState.GROUP_AFTER
+            case ParamState.RIGHT_SQUARE_AFTER if not self.group_stack:
+                self.parameter_state = ParamState.GROUP_AFTER
             case st:
                 fail(f"Function {function.name!r} "
                      f"has an unsupported group configuration. "
                      f"(Unexpected state {st}.b)")
-        self.group += 1
+        self.group_count += 1
+        self.group_stack.append(self.group_count)
         function.docstring_only = True
 
     def parse_closing_square_bracket(self, function: Function) -> None:
         """Parse closing parameter group symbol ']'."""
-        if not self.group:
+        if not self.group_stack:
             fail(f"Function {function.name!r} has a ']' without a matching 
'['.")
-        if not any(p.group == self.group for p in 
function.parameters.values()):
+        group = self.group_stack.pop()
+        if not any(p.group == group for p in function.parameters.values()):
             fail(f"Function {function.name!r} has an empty group. "
                  "All groups must contain at least one parameter.")
-        self.group -= 1
         match self.parameter_state:
             case ParamState.LEFT_SQUARE_BEFORE | ParamState.GROUP_BEFORE:
                 self.parameter_state = ParamState.GROUP_BEFORE
@@ -1268,7 +1281,7 @@ def parse_slash(self, function: Function, version: 
VersionTuple | None) -> None:
             ParamState.RIGHT_SQUARE_AFTER,
             ParamState.GROUP_BEFORE,
         }
-        if (self.parameter_state not in allowed) or self.group:
+        if (self.parameter_state not in allowed) or self.group_stack:
             fail(f"Function {function.name!r} has an unsupported group 
configuration. "
                  f"(Unexpected state {self.parameter_state}.d)")
         # fixup preceding parameters
@@ -1329,7 +1342,7 @@ def state_parameter_docstring(self, line: str) -> None:
     def state_function_docstring(self, line: str) -> None:
         assert self.function is not None
 
-        if self.group:
+        if self.group_stack:
             fail(f"Function {self.function.name!r} has a ']' without a 
matching '['.")
 
         if not self.valid_line(line):
@@ -1364,16 +1377,25 @@ def format_docstring_signature(
                 else:
                     assert positional_only
                 if positional_only:
-                    p.right_bracket_count = abs(p.group)
+                    p.right_bracket_count = p.group_depth
                 else:
                     # don't put any right brackets around non-positional-only 
parameters, ever.
                     p.right_bracket_count = 0
 
             right_bracket_count = 0
+            last_group = 0
 
-            def fix_right_bracket_count(desired: int) -> str:
-                nonlocal right_bracket_count
+            def fix_right_bracket_count(desired: int, group: int = 0) -> str:
+                nonlocal right_bracket_count, last_group
                 s = ''
+                if (group != last_group and right_bracket_count and
+                    ((desired >= right_bracket_count) if group < 0 else
+                     (desired <= right_bracket_count))):
+                    # The group is not nested in the previous group,
+                    # close the brackets of the latter first.
+                    s += ']' * right_bracket_count
+                    right_bracket_count = 0
+                last_group = group
                 while right_bracket_count < desired:
                     s += '['
                     right_bracket_count += 1
@@ -1441,7 +1463,8 @@ def add_parameter(text: str) -> None:
                     added_star = True
                     add_parameter('*,')
 
-                p_lines = [fix_right_bracket_count(p.right_bracket_count)]
+                p_lines = [fix_right_bracket_count(p.right_bracket_count,
+                                                   p.group)]
 
                 if isinstance(p.converter, self_converter):
                     # annotate first parameter as being a "self".
diff --git a/Tools/clinic/libclinic/function.py 
b/Tools/clinic/libclinic/function.py
index 1c643caea98e3b..325633eb010608 100644
--- a/Tools/clinic/libclinic/function.py
+++ b/Tools/clinic/libclinic/function.py
@@ -205,7 +205,11 @@ class Parameter:
     converter: CConverter
     annotation: object = inspect.Parameter.empty
     docstring: str = ''
+    # Identifier of the optional group containing the parameter (0 if none).
+    # It is negative for groups before the required parameters.
     group: int = 0
+    # Nesting level of that group (0 if none).
+    group_depth: int = 0
     # (`None` signifies that there is no deprecation)
     deprecated_positional: VersionTuple | None = None
     deprecated_keyword: VersionTuple | None = None
@@ -301,15 +305,18 @@ def permute_right_option_groups(
 
 
 def permute_optional_groups(
-    left: Sequence[Iterable[Parameter]],
+    left: Sequence[Sequence[Iterable[Parameter]]],
     required: Iterable[Parameter],
-    right: Sequence[Iterable[Parameter]]
+    right: Sequence[Sequence[Iterable[Parameter]]]
 ) -> tuple[ParamTuple, ...]:
     """
     Generator function that computes the set of acceptable
     argument lists for the provided iterables of
     argument groups.  (Actually it generates a tuple of tuples.)
 
+    "left" and "right" are sequences of chains of nested groups.
+    Groups of different chains are independent of each other.
+
     Algorithm: prefer left options over right options.
 
     If required is empty, left must also be empty.
@@ -319,10 +326,21 @@ def permute_optional_groups(
         if left:
             raise ValueError("required is empty but left is not")
 
+    left_options: list[ParamTuple] = [()]
+    for chain in left:
+        left_options = [option + t
+                        for option in left_options
+                        for t in permute_left_option_groups(chain)]
+    right_options: list[ParamTuple] = [()]
+    for chain in reversed(right):
+        right_options = [t + option
+                         for option in right_options
+                         for t in permute_right_option_groups(chain)]
+
     accumulator: list[ParamTuple] = []
     counts = set()
-    for r in permute_right_option_groups(right):
-        for l in permute_left_option_groups(left):
+    for r in right_options:
+        for l in left_options:
             t = l + required + r
             if len(t) in counts:
                 continue

_______________________________________________
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