Skip to content

Commit c8b7c46

Browse files
gh-64490: Tweak AC implementation for **kwds (GH-139133)
Check for parameters following a var-keyword parameter in one place instead of three, and improve the error messages for a var-keyword parameter used with non-positional parameters and for a var-positional or var-keyword parameter in an optional group. Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 65e206c commit c8b7c46

2 files changed

Lines changed: 82 additions & 38 deletions

File tree

‎Lib/test/test_clinic.py‎

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -379,28 +379,26 @@ def test_vararg_after_star(self):
379379
"""
380380
self.expect_failure(block, err, lineno=6)
381381

382-
def test_double_star_after_var_keyword(self):
383-
err = "Function 'my_test_func' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
382+
def test_parameter_after_var_keyword(self):
383+
err = "parameters cannot follow var-keyword parameter: 'invalid_arg: object'"
384384
block = """
385385
/*[clinic input]
386386
my_test_func
387387
388-
pos_arg: object
389388
**kwds: dict
390-
**
389+
invalid_arg: object
391390
[clinic start generated code]*/
392391
"""
393392
self.expect_failure(block, err, lineno=5)
394393

395-
def test_var_keyword_after_star(self):
394+
def test_double_star_without_name(self):
396395
err = "Function 'my_test_func' has an invalid parameter declaration: '**'"
397396
block = """
398397
/*[clinic input]
399398
my_test_func
400399
401400
pos_arg: object
402401
**
403-
**kwds: dict
404402
[clinic start generated code]*/
405403
"""
406404
self.expect_failure(block, err, lineno=5)
@@ -2128,6 +2126,42 @@ def test_disallowed_grouping__varpos(self):
21282126
"""
21292127
self.expect_failure(block, err, lineno=4)
21302128

2129+
def test_disallowed_grouping__parameter_after_group(self):
2130+
# Only positional-only parameters can follow an optional group.
2131+
group_err = ("You cannot use optional groups ('[' and ']') unless all "
2132+
"parameters are positional-only ('/')")
2133+
kwds_err = ("cannot use a var-keyword parameter with pos-or-keyword "
2134+
"or keyword-only parameters")
2135+
dataset = (("""
2136+
module foo
2137+
foo.bar
2138+
[
2139+
a: int
2140+
b: int
2141+
]
2142+
y: int
2143+
""", group_err), ("""
2144+
module foo
2145+
foo.bar
2146+
[
2147+
a: int
2148+
b: int
2149+
]
2150+
*
2151+
y: int
2152+
""", group_err), ("""
2153+
module foo
2154+
foo.bar
2155+
[
2156+
a: int
2157+
b: int
2158+
]
2159+
**kwds: dict
2160+
""", kwds_err))
2161+
for block, err in dataset:
2162+
with self.subTest(block=block):
2163+
self.expect_failure(block, err)
2164+
21312165
def test_disallowed_grouping__must_be_position_only(self):
21322166
dataset = ("""
21332167
with_kwds
@@ -2140,11 +2174,6 @@ def test_disallowed_grouping__must_be_position_only(self):
21402174
[
21412175
a: object
21422176
]
2143-
""", """
2144-
with_kwds
2145-
[
2146-
**kwds: dict
2147-
]
21482177
""")
21492178
err = (
21502179
"You cannot use optional groups ('[' and ']') unless all "
@@ -2654,38 +2683,50 @@ def test_slash_after_var_keyword(self):
26542683
block = """
26552684
module foo
26562685
foo.bar
2657-
x: int
2658-
y: int
26592686
**kwds: dict
2660-
z: int
26612687
/
26622688
"""
2663-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
2689+
err = "parameters cannot follow var-keyword parameter: '/'"
26642690
self.expect_failure(block, err)
26652691

26662692
def test_star_after_var_keyword(self):
26672693
block = """
26682694
module foo
26692695
foo.bar
2670-
x: int
2671-
y: int
26722696
**kwds: dict
2673-
z: int
26742697
*
26752698
"""
2676-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
2699+
err = "parameters cannot follow var-keyword parameter: '*'"
26772700
self.expect_failure(block, err)
26782701

26792702
def test_parameter_after_var_keyword(self):
26802703
block = """
26812704
module foo
26822705
foo.bar
2683-
x: int
2684-
y: int
26852706
**kwds: dict
26862707
z: int
26872708
"""
2688-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
2709+
err = "parameters cannot follow var-keyword parameter: 'z: int'"
2710+
self.expect_failure(block, err)
2711+
2712+
def test_group_with_var_keyword(self):
2713+
block = """
2714+
with_kwds
2715+
[
2716+
**kwds: dict
2717+
]
2718+
"""
2719+
err = "A var-keyword parameter cannot be in an optional group."
2720+
self.expect_failure(block, err)
2721+
2722+
def test_group_with_var_positional(self):
2723+
block = """
2724+
with_varpos
2725+
[
2726+
*args: tuple
2727+
]
2728+
"""
2729+
err = "A var-positional parameter cannot be in an optional group."
26892730
self.expect_failure(block, err)
26902731

26912732
def test_depr_star_must_come_after_slash(self):
@@ -2777,7 +2818,7 @@ def test_parameters_no_more_than_one_vararg(self):
27772818
self.expect_failure(block, err, lineno=3)
27782819

27792820
def test_parameters_no_more_than_one_var_keyword(self):
2780-
err = "Encountered parameter line when not expecting parameters: **var_keyword_2: dict"
2821+
err = "parameters cannot follow var-keyword parameter: '**var_keyword_2: dict'"
27812822
block = """
27822823
module foo
27832824
foo.bar
@@ -3587,7 +3628,8 @@ def test_var_keyword_with_pos_or_kw(self):
35873628
x: int
35883629
**kwds: dict
35893630
"""
3590-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
3631+
err = ("Function 'bar' cannot use a var-keyword parameter with "
3632+
"pos-or-keyword or keyword-only parameters.")
35913633
self.expect_failure(block, err)
35923634

35933635
def test_var_keyword_with_kw_only(self):
@@ -3600,7 +3642,8 @@ def test_var_keyword_with_kw_only(self):
36003642
y: int
36013643
**kwds: dict
36023644
"""
3603-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
3645+
err = ("Function 'bar' cannot use a var-keyword parameter with "
3646+
"pos-or-keyword or keyword-only parameters.")
36043647
self.expect_failure(block, err)
36053648

36063649
def test_var_keyword_with_pos_or_kw_and_kw_only(self):
@@ -3614,7 +3657,8 @@ def test_var_keyword_with_pos_or_kw_and_kw_only(self):
36143657
z: int
36153658
**kwds: dict
36163659
"""
3617-
err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'"
3660+
err = ("Function 'bar' cannot use a var-keyword parameter with "
3661+
"pos-or-keyword or keyword-only parameters.")
36183662
self.expect_failure(block, err)
36193663

36203664
def test_allow_negative_accepted_by_py_ssize_t_converter_only(self):

‎Tools/clinic/libclinic/dsl_parser.py‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,9 @@ def state_parameter(self, line: str) -> None:
951951
self.deprecated_until = self.parse_version(match[1], 'until')
952952
line = match[2]
953953

954+
if not self.expecting_parameters:
955+
fail(f'parameters cannot follow var-keyword parameter: {line!r}')
956+
954957
func = self.function
955958
match line:
956959
case '*':
@@ -967,10 +970,6 @@ def state_parameter(self, line: str) -> None:
967970
def parse_parameter(self, line: str) -> None:
968971
assert self.function is not None
969972

970-
if not self.expecting_parameters:
971-
fail('Encountered parameter line when not expecting '
972-
f'parameters: {line}')
973-
974973
match self.parameter_state:
975974
case ParamState.START | ParamState.REQUIRED:
976975
self.to_required()
@@ -1021,8 +1020,9 @@ def parse_parameter(self, line: str) -> None:
10211020
for p in self.function.parameters.values()
10221021
)
10231022
if has_non_positional_param:
1024-
fail(f"Function {self.function.name!r} has an "
1025-
f"invalid parameter declaration (**kwargs?): {line!r}")
1023+
fail(f'Function {self.function.name!r} cannot use a var-keyword '
1024+
f'parameter with pos-or-keyword or keyword-only '
1025+
f'parameters.')
10261026
is_var_keyword = True
10271027
parameter = function_args.kwarg
10281028
else:
@@ -1161,6 +1161,9 @@ def bad_node(self, node: ast.AST) -> None:
11611161

11621162
kind: inspect._ParameterKind
11631163
if is_vararg:
1164+
if self.group_stack:
1165+
fail("A var-positional parameter cannot be in an optional "
1166+
"group.")
11641167
if any(p.group for p in self.function.parameters.values()):
11651168
# With "foo([a, b], *args)" the number of arguments does not
11661169
# tell whether the group is passed or all arguments belong
@@ -1169,6 +1172,9 @@ def bad_node(self, node: ast.AST) -> None:
11691172
f"groups with a var-positional parameter.")
11701173
kind = inspect.Parameter.VAR_POSITIONAL
11711174
elif is_var_keyword:
1175+
if self.group_stack:
1176+
fail("A var-keyword parameter cannot be in an optional "
1177+
"group.")
11721178
kind = inspect.Parameter.VAR_KEYWORD
11731179
elif self.keyword_only:
11741180
kind = inspect.Parameter.KEYWORD_ONLY
@@ -1287,9 +1293,6 @@ def parse_star(self, function: Function, version: VersionTuple | None) -> None:
12871293
The 'version' parameter signifies the future version from which
12881294
the marker will take effect (None means it is already in effect).
12891295
"""
1290-
if not self.expecting_parameters:
1291-
fail("Encountered '*' when not expecting parameters")
1292-
12931296
if version is None:
12941297
self.check_previous_star()
12951298
self.check_remaining_star()
@@ -1356,9 +1359,6 @@ def parse_slash(self, function: Function, version: VersionTuple | None) -> None:
13561359
The 'version' parameter signifies the future version from which
13571360
the marker will take effect (None means it is already in effect).
13581361
"""
1359-
if not self.expecting_parameters:
1360-
fail("Encountered '/' when not expecting parameters")
1361-
13621362
if version is None:
13631363
if self.deprecated_keyword:
13641364
fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'")

0 commit comments

Comments
 (0)