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 9a8faf777869d4..0a0dc4115e15ec 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 = """ @@ -4047,6 +4100,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) @@ -4649,21 +4722,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"), @@ -4671,21 +4744,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"), @@ -4693,7 +4766,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. @@ -4701,7 +4774,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. @@ -4710,11 +4783,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