Skip to content

Commit c8bee19

Browse files
gh-64502: Support several optional groups on the same level in Argument Clinic
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) <noreply@anthropic.com>
1 parent caac927 commit c8bee19

8 files changed

Lines changed: 460 additions & 64 deletions

File tree

Lib/test/clinic.test.c

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5830,6 +5830,85 @@ group_and_optional_parameter_impl(PyObject *module, int group_left_1,
58305830
/*[clinic end generated code: output=3faea69eafd5bbbe input=7f0fbb6124f5a972]*/
58315831

58325832

5833+
/*[clinic input]
5834+
two_groups_on_the_same_level
5835+
[
5836+
a: object
5837+
b: object
5838+
]
5839+
[
5840+
c: object
5841+
]
5842+
d: object
5843+
/
5844+
Groups on the same level are independent of each other.
5845+
[clinic start generated code]*/
5846+
5847+
PyDoc_STRVAR(two_groups_on_the_same_level__doc__,
5848+
"two_groups_on_the_same_level([a, b,] [c,] d)\n"
5849+
"Groups on the same level are independent of each other.");
5850+
5851+
#define TWO_GROUPS_ON_THE_SAME_LEVEL_METHODDEF \
5852+
{"two_groups_on_the_same_level", (PyCFunction)two_groups_on_the_same_level, METH_VARARGS, two_groups_on_the_same_level__doc__},
5853+
5854+
static PyObject *
5855+
two_groups_on_the_same_level_impl(PyObject *module, int group_left_1,
5856+
PyObject *a, PyObject *b, int group_left_2,
5857+
PyObject *c, PyObject *d);
5858+
5859+
static PyObject *
5860+
two_groups_on_the_same_level(PyObject *module, PyObject *args)
5861+
{
5862+
PyObject *return_value = NULL;
5863+
int group_left_1 = 0;
5864+
PyObject *a = NULL;
5865+
PyObject *b = NULL;
5866+
int group_left_2 = 0;
5867+
PyObject *c = NULL;
5868+
PyObject *d;
5869+
5870+
switch (PyTuple_GET_SIZE(args)) {
5871+
case 1:
5872+
if (!PyArg_ParseTuple(args, "O:two_groups_on_the_same_level", &d)) {
5873+
goto exit;
5874+
}
5875+
break;
5876+
case 2:
5877+
if (!PyArg_ParseTuple(args, "OO:two_groups_on_the_same_level", &c, &d)) {
5878+
goto exit;
5879+
}
5880+
group_left_2 = 1;
5881+
break;
5882+
case 3:
5883+
if (!PyArg_ParseTuple(args, "OOO:two_groups_on_the_same_level", &a, &b, &d)) {
5884+
goto exit;
5885+
}
5886+
group_left_1 = 1;
5887+
break;
5888+
case 4:
5889+
if (!PyArg_ParseTuple(args, "OOOO:two_groups_on_the_same_level", &a, &b, &c, &d)) {
5890+
goto exit;
5891+
}
5892+
group_left_1 = 1;
5893+
group_left_2 = 1;
5894+
break;
5895+
default:
5896+
PyErr_SetString(PyExc_TypeError, "two_groups_on_the_same_level requires 1 to 4 arguments");
5897+
goto exit;
5898+
}
5899+
return_value = two_groups_on_the_same_level_impl(module, group_left_1, a, b, group_left_2, c, d);
5900+
5901+
exit:
5902+
return return_value;
5903+
}
5904+
5905+
static PyObject *
5906+
two_groups_on_the_same_level_impl(PyObject *module, int group_left_1,
5907+
PyObject *a, PyObject *b, int group_left_2,
5908+
PyObject *c, PyObject *d)
5909+
/*[clinic end generated code: output=508a61ee582da21e input=1b45d9b675b32d1a]*/
5910+
5911+
58335912
/*[clinic input]
58345913
Test._pyarg_parsestackandkeywords
58355914
cls: defining_class

Lib/test/test_clinic.py

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -835,15 +835,15 @@ def _test(self, l, m, r, output):
835835
self.assertEqual(output, computed)
836836

837837
def test_range(self):
838-
self._test([['start']], ['stop'], [['step']],
838+
self._test([[['start']]], ['stop'], [[['step']]],
839839
(
840840
('stop',),
841841
('start', 'stop',),
842842
('start', 'stop', 'step',),
843843
))
844844

845845
def test_add_window(self):
846-
self._test([['x', 'y']], ['ch'], [['attr']],
846+
self._test([[['x', 'y']]], ['ch'], [[['attr']]],
847847
(
848848
('ch',),
849849
('ch', 'attr'),
@@ -852,7 +852,8 @@ def test_add_window(self):
852852
))
853853

854854
def test_ludicrous(self):
855-
self._test([['a1', 'a2', 'a3'], ['b1', 'b2']], ['c1'], [['d1', 'd2'], ['e1', 'e2', 'e3']],
855+
self._test([[['a1', 'a2', 'a3'], ['b1', 'b2']]], ['c1'],
856+
[[['d1', 'd2'], ['e1', 'e2', 'e3']]],
856857
(
857858
('c1',),
858859
('b1', 'b2', 'c1'),
@@ -863,17 +864,36 @@ def test_ludicrous(self):
863864
))
864865

865866
def test_right_only(self):
866-
self._test([], [], [['a'],['b'],['c']],
867+
self._test([], [], [[['a'],['b'],['c']]],
867868
(
868869
(),
869870
('a',),
870871
('a', 'b'),
871872
('a', 'b', 'c')
872873
))
873874

875+
def test_chgat(self):
876+
# Two independent groups on the left.
877+
self._test([[['y', 'x']], [['n']]], ['attr'], [],
878+
(
879+
('attr',),
880+
('n', 'attr'),
881+
('y', 'x', 'attr'),
882+
('y', 'x', 'n', 'attr'),
883+
))
884+
885+
def test_independent_groups_on_the_right(self):
886+
self._test([], ['a'], [[['b']], [['c', 'd']]],
887+
(
888+
('a',),
889+
('a', 'b'),
890+
('a', 'c', 'd'),
891+
('a', 'b', 'c', 'd'),
892+
))
893+
874894
def test_have_left_options_but_required_is_empty(self):
875895
def fn():
876-
permute_optional_groups(['a'], [], [])
896+
permute_optional_groups([[['a']]], [], [])
877897
self.assertRaises(ValueError, fn)
878898

879899

@@ -1714,41 +1734,74 @@ def test_nested_groups(self):
17141734
Attributes for the character.
17151735
""")
17161736

1717-
def test_disallowed_grouping__two_top_groups_on_left(self):
1718-
err = (
1719-
"Function 'two_top_groups_on_left' has an unsupported group "
1720-
"configuration. (Unexpected state 2.b)"
1721-
)
1722-
block = """
1723-
module foo
1724-
foo.two_top_groups_on_left
1737+
def test_two_top_groups_on_left(self):
1738+
function = self.parse_function("""
1739+
module curses
1740+
curses.chgat
17251741
[
1726-
group1 : int
1742+
y: int
1743+
Y-coordinate.
1744+
x: int
1745+
X-coordinate.
17271746
]
17281747
[
1729-
group2 : int
1748+
num: int
1749+
Number of characters.
17301750
]
1731-
param: int
1732-
"""
1733-
self.expect_failure(block, err, lineno=5)
1751+
attr: long
1752+
Attributes for the characters.
1753+
/
1754+
""")
1755+
dataset = (
1756+
('y', -1), ('x', -1),
1757+
('num', -2),
1758+
('attr', 0),
1759+
)
1760+
for name, group in dataset:
1761+
with self.subTest(name=name, group=group):
1762+
p = function.parameters[name]
1763+
self.assertEqual(p.group, group)
1764+
self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
1765+
self.checkDocstring(function, """
1766+
chgat([y, x,] [num,] attr)
17341767
1735-
def test_disallowed_grouping__two_top_groups_on_right(self):
1736-
block = """
1768+
1769+
y
1770+
Y-coordinate.
1771+
x
1772+
X-coordinate.
1773+
num
1774+
Number of characters.
1775+
attr
1776+
Attributes for the characters.
1777+
""")
1778+
1779+
def test_two_top_groups_on_right(self):
1780+
function = self.parse_function("""
17371781
module foo
17381782
foo.two_top_groups_on_right
17391783
param: int
17401784
[
1741-
group1 : int
1785+
group1: int
17421786
]
17431787
[
1744-
group2 : int
1788+
group2: int
17451789
]
1746-
"""
1747-
err = (
1748-
"Function 'two_top_groups_on_right' has an unsupported group "
1749-
"configuration. (Unexpected state 6.b)"
1790+
/
1791+
""")
1792+
dataset = (
1793+
('param', 0),
1794+
('group1', 1),
1795+
('group2', 2),
17501796
)
1751-
self.expect_failure(block, err)
1797+
for name, group in dataset:
1798+
with self.subTest(name=name, group=group):
1799+
p = function.parameters[name]
1800+
self.assertEqual(p.group, group)
1801+
self.assertEqual(p.kind, inspect.Parameter.POSITIONAL_ONLY)
1802+
self.checkDocstring(function, """
1803+
two_top_groups_on_right(param, [group1,] [group2])
1804+
""")
17521805

17531806
def test_disallowed_grouping__parameter_after_group_on_right(self):
17541807
block = """
@@ -3904,6 +3957,26 @@ def test_group_and_two_opt(self):
39043957
self.assertEqual(fn(1, 2, 3, 4, 5), (True, 1, 2, 3, 4, 5))
39053958
self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5, 6)
39063959

3960+
def test_two_groups_on_left(self):
3961+
# fn([a, b,] [c,] d)
3962+
fn = ac_tester.two_groups_on_left
3963+
self.assertRaises(TypeError, fn)
3964+
self.assertEqual(fn(1), (False, None, None, False, None, 1))
3965+
self.assertEqual(fn(1, 2), (False, None, None, True, 1, 2))
3966+
self.assertEqual(fn(1, 2, 3), (True, 1, 2, False, None, 3))
3967+
self.assertEqual(fn(1, 2, 3, 4), (True, 1, 2, True, 3, 4))
3968+
self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5)
3969+
3970+
def test_two_groups_on_right(self):
3971+
# fn(a, [b,] [c, d])
3972+
fn = ac_tester.two_groups_on_right
3973+
self.assertRaises(TypeError, fn)
3974+
self.assertEqual(fn(1), (1, False, None, False, None, None))
3975+
self.assertEqual(fn(1, 2), (1, True, 2, False, None, None))
3976+
self.assertEqual(fn(1, 2, 3), (1, False, None, True, 2, 3))
3977+
self.assertEqual(fn(1, 2, 3, 4), (1, True, 2, True, 3, 4))
3978+
self.assertRaises(TypeError, fn, 1, 2, 3, 4, 5)
3979+
39073980
def test_gh_32092_oob(self):
39083981
ac_tester.gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)
39093982

@@ -4506,59 +4579,59 @@ def test_permute_optional_groups(self):
45064579
"expected": ((),),
45074580
}
45084581
noleft1 = {
4509-
"left": (), "required": ("b",), "right": ("c",),
4582+
"left": (), "required": ("b",), "right": (("c",),),
45104583
"expected": (
45114584
("b",),
45124585
("b", "c"),
45134586
),
45144587
}
45154588
noleft2 = {
4516-
"left": (), "required": ("b", "c",), "right": ("d",),
4589+
"left": (), "required": ("b", "c",), "right": (("d",),),
45174590
"expected": (
45184591
("b", "c"),
45194592
("b", "c", "d"),
45204593
),
45214594
}
45224595
noleft3 = {
4523-
"left": (), "required": ("b", "c",), "right": ("d", "e"),
4596+
"left": (), "required": ("b", "c",), "right": (("d", "e"),),
45244597
"expected": (
45254598
("b", "c"),
45264599
("b", "c", "d"),
45274600
("b", "c", "d", "e"),
45284601
),
45294602
}
45304603
noright1 = {
4531-
"left": ("a",), "required": ("b",), "right": (),
4604+
"left": (("a",),), "required": ("b",), "right": (),
45324605
"expected": (
45334606
("b",),
45344607
("a", "b"),
45354608
),
45364609
}
45374610
noright2 = {
4538-
"left": ("a",), "required": ("b", "c"), "right": (),
4611+
"left": (("a",),), "required": ("b", "c"), "right": (),
45394612
"expected": (
45404613
("b", "c"),
45414614
("a", "b", "c"),
45424615
),
45434616
}
45444617
noright3 = {
4545-
"left": ("a", "b"), "required": ("c",), "right": (),
4618+
"left": (("a", "b"),), "required": ("c",), "right": (),
45464619
"expected": (
45474620
("c",),
45484621
("b", "c"),
45494622
("a", "b", "c"),
45504623
),
45514624
}
45524625
leftandright1 = {
4553-
"left": ("a",), "required": ("b",), "right": ("c",),
4626+
"left": (("a",),), "required": ("b",), "right": (("c",),),
45544627
"expected": (
45554628
("b",),
45564629
("a", "b"), # Prefer left.
45574630
("a", "b", "c"),
45584631
),
45594632
}
45604633
leftandright2 = {
4561-
"left": ("a", "b"), "required": ("c", "d"), "right": ("e", "f"),
4634+
"left": (("a", "b"),), "required": ("c", "d"), "right": (("e", "f"),),
45624635
"expected": (
45634636
("c", "d"),
45644637
("b", "c", "d"), # Prefer left.
@@ -4567,11 +4640,28 @@ def test_permute_optional_groups(self):
45674640
("a", "b", "c", "d", "e", "f"),
45684641
),
45694642
}
4643+
independentleft = {
4644+
"left": (("a",), ("b",)), "required": ("c",), "right": (),
4645+
"expected": (
4646+
("c",),
4647+
("b", "c"),
4648+
("a", "b", "c"),
4649+
),
4650+
}
4651+
independentright = {
4652+
"left": (), "required": ("a",), "right": (("b",), ("c",)),
4653+
"expected": (
4654+
("a",),
4655+
("a", "b"),
4656+
("a", "b", "c"),
4657+
),
4658+
}
45704659
dataset = (
45714660
empty,
45724661
noleft1, noleft2, noleft3,
45734662
noright1, noright2, noright3,
45744663
leftandright1, leftandright2,
4664+
independentleft, independentright,
45754665
)
45764666
for params in dataset:
45774667
with self.subTest(**params):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Argument Clinic now supports several optional groups on the same nesting
2+
level, like in ``[y, x,] [n,] attr``.
3+
Such groups can be omitted independently of each other.

0 commit comments

Comments
 (0)