From 6d016f18ced0e46353bb501226144228689afd7c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:26:04 +0100 Subject: [PATCH 1/5] Address Serhiy's follow-up comments --- Lib/test/test_clinic.py | 63 ++++++++++++---------------- Tools/clinic/libclinic/dsl_parser.py | 18 +++----- 2 files changed, 32 insertions(+), 49 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index d54dd546ea36fb0..9c7b6eb81ee58e1 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -358,30 +358,16 @@ def test_vararg_after_star(self): self.expect_failure(block, err, lineno=6) def test_double_star_after_var_keyword(self): - err = "Function 'my_test_func' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = "parameters cannot follow var-keyword parameter: 'invalid_arg: object'" block = """ /*[clinic input] my_test_func - pos_arg: object **kwds: dict - ** + invalid_arg: object [clinic start generated code]*/ """ - self.expect_failure(block, err, lineno=5) - - def test_var_keyword_after_star(self): - err = "Function 'my_test_func' has an invalid parameter declaration: '**'" - block = """ - /*[clinic input] - my_test_func - - pos_arg: object - ** - **kwds: dict - [clinic start generated code]*/ - """ - self.expect_failure(block, err, lineno=5) + self.expect_failure(block, err, lineno=7) def test_module_already_got_one(self): err = "Already defined module 'm'!" @@ -1644,11 +1630,6 @@ def test_disallowed_grouping__must_be_position_only(self): [ a: object ] - """, """ - with_kwds - [ - **kwds: dict - ] """) err = ( "You cannot use optional groups ('[' and ']') unless all " @@ -2036,38 +2017,40 @@ def test_slash_after_var_keyword(self): block = """ module foo foo.bar - x: int - y: int **kwds: dict - z: int / """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = "parameters cannot follow var-keyword parameter: '/'" self.expect_failure(block, err) def test_star_after_var_keyword(self): block = """ module foo foo.bar - x: int - y: int **kwds: dict - z: int * """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = "parameters cannot follow var-keyword parameter: '*'" self.expect_failure(block, err) def test_parameter_after_var_keyword(self): block = """ module foo foo.bar - x: int - y: int **kwds: dict z: int """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = "parameters cannot follow var-keyword parameter: 'z: int'" + self.expect_failure(block, err) + + def test_group_with_var_keyword(self): + block = """ + with_kwds + [ + **kwds: dict + ] + """ + err = "parameters cannot follow var-keyword parameter: ']'" self.expect_failure(block, err) def test_depr_star_must_come_after_slash(self): @@ -2159,7 +2142,7 @@ def test_parameters_no_more_than_one_vararg(self): self.expect_failure(block, err, lineno=3) def test_parameters_no_more_than_one_var_keyword(self): - err = "Encountered parameter line when not expecting parameters: **var_keyword_2: dict" + err = "parameters cannot follow var-keyword parameter: '**var_keyword_2: dict'" block = """ module foo foo.bar @@ -2714,7 +2697,9 @@ def test_var_keyword_with_pos_or_kw(self): x: int **kwds: dict """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = ("Function 'bar' uses a var-keyword parameter and other " + "non-positional parameters, which Argument Clinic does " + "not currently support: '**kwds: dict'") self.expect_failure(block, err) def test_var_keyword_with_kw_only(self): @@ -2727,7 +2712,9 @@ def test_var_keyword_with_kw_only(self): y: int **kwds: dict """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = ("Function 'bar' uses a var-keyword parameter and other " + "non-positional parameters, which Argument Clinic does " + "not currently support: '**kwds: dict'") self.expect_failure(block, err) def test_var_keyword_with_pos_or_kw_and_kw_only(self): @@ -2741,7 +2728,9 @@ def test_var_keyword_with_pos_or_kw_and_kw_only(self): z: int **kwds: dict """ - err = "Function 'bar' has an invalid parameter declaration (**kwargs?): '**kwds: dict'" + err = ("Function 'bar' uses a var-keyword parameter and other " + "non-positional parameters, which Argument Clinic does " + "not currently support: '**kwds: dict'") self.expect_failure(block, err) def test_allow_negative_accepted_by_py_ssize_t_converter_only(self): diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 0d83baeba9e5080..8ea4151fe7851cb 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -862,6 +862,9 @@ def state_parameter(self, line: str) -> None: line = match[1] version = self.parse_version(match[2]) + if not self.expecting_parameters: + fail(f'parameters cannot follow var-keyword parameter: {line!r}') + func = self.function match line: case '*': @@ -878,10 +881,6 @@ def state_parameter(self, line: str) -> None: def parse_parameter(self, line: str) -> None: assert self.function is not None - if not self.expecting_parameters: - fail('Encountered parameter line when not expecting ' - f'parameters: {line}') - match self.parameter_state: case ParamState.START | ParamState.REQUIRED: self.to_required() @@ -932,8 +931,9 @@ def parse_parameter(self, line: str) -> None: for p in self.function.parameters.values() ) if has_non_positional_param: - fail(f"Function {self.function.name!r} has an " - f"invalid parameter declaration (**kwargs?): {line!r}") + fail(f'Function {self.function.name!r} uses a var-keyword parameter ' + f'and other non-positional parameters, which Argument Clinic ' + f'does not currently support: {line!r}') is_var_keyword = True parameter = function_args.kwarg else: @@ -1182,9 +1182,6 @@ def parse_star(self, function: Function, version: VersionTuple | None) -> None: The 'version' parameter signifies the future version from which the marker will take effect (None means it is already in effect). """ - if not self.expecting_parameters: - fail("Encountered '*' when not expecting parameters") - if version is None: self.check_previous_star() self.check_remaining_star() @@ -1240,9 +1237,6 @@ def parse_slash(self, function: Function, version: VersionTuple | None) -> None: The 'version' parameter signifies the future version from which the marker will take effect (None means it is already in effect). """ - if not self.expecting_parameters: - fail("Encountered '/' when not expecting parameters") - if version is None: if self.deprecated_keyword: fail(f"Function {function.name!r}: '/' must precede '/ [from ...]'") From 88db6df8a861a6f94d5acdb1acff966a9d0ca420 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:59:28 +0100 Subject: [PATCH 2/5] fixup! Address Serhiy's follow-up comments --- Lib/test/test_clinic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 9c7b6eb81ee58e1..fc510101d3726b3 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -367,7 +367,7 @@ def test_double_star_after_var_keyword(self): invalid_arg: object [clinic start generated code]*/ """ - self.expect_failure(block, err, lineno=7) + self.expect_failure(block, err, lineno=5) def test_module_already_got_one(self): err = "Already defined module 'm'!" From c77c41232d4e8effaafaee19adeb20fe013cad7b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 22 Aug 2026 11:48:30 +0300 Subject: [PATCH 3/5] Add tests for a bare ** and for var-positional in an optional group --- Lib/test/test_clinic.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 4c480f2d7d9f83d..75dbe263b9591c0 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -377,7 +377,7 @@ def test_vararg_after_star(self): """ self.expect_failure(block, err, lineno=6) - def test_double_star_after_var_keyword(self): + def test_parameter_after_var_keyword(self): err = "parameters cannot follow var-keyword parameter: 'invalid_arg: object'" block = """ /*[clinic input] @@ -389,6 +389,18 @@ def test_double_star_after_var_keyword(self): """ self.expect_failure(block, err, lineno=5) + def test_double_star_without_name(self): + err = "Function 'my_test_func' has an invalid parameter declaration: '**'" + block = """ + /*[clinic input] + my_test_func + + pos_arg: object + ** + [clinic start generated code]*/ + """ + self.expect_failure(block, err, lineno=5) + def test_module_already_got_one(self): err = "Already defined module 'm'!" block = """ @@ -2403,6 +2415,17 @@ def test_group_with_var_keyword(self): err = "parameters cannot follow var-keyword parameter: ']'" self.expect_failure(block, err) + def test_group_with_var_positional(self): + block = """ + with_varpos + [ + *args: tuple + ] + """ + err = ("You cannot use optional groups ('[' and ']') unless all " + "parameters are positional-only ('/')") + self.expect_failure(block, err) + def test_depr_star_must_come_after_slash(self): block = """ module foo From 7938b67cffe09d4cee44df85fc6c19d6aeb0e372 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 22 Aug 2026 11:56:17 +0300 Subject: [PATCH 4/5] Add tests for parameters after an optional group --- Lib/test/test_clinic.py | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 75dbe263b9591c0..d73418804a784a5 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -1980,6 +1980,51 @@ def test_disallowed_grouping__no_matching_bracket(self): err = "Function 'empty_group' has a ']' without a matching '['" self.expect_failure(block, err) + def test_disallowed_grouping__parameter_after_group(self): + # Only positional-only parameters can follow an optional group. + group_err = ("You cannot use optional groups ('[' and ']') unless all " + "parameters are positional-only ('/')") + kwds_err = ("Function 'bar' uses a var-keyword parameter and other " + "non-positional parameters, which Argument Clinic does " + "not currently support: '**kwds: dict'") + dataset = ((""" + module foo + foo.bar + [ + a: int + b: int + ] + y: int + """, group_err), (""" + module foo + foo.bar + [ + a: int + b: int + ] + * + y: int + """, group_err), (""" + module foo + foo.bar + [ + a: int + b: int + ] + *args: tuple + """, group_err), (""" + module foo + foo.bar + [ + a: int + b: int + ] + **kwds: dict + """, kwds_err)) + for block, err in dataset: + with self.subTest(block=block): + self.expect_failure(block, err) + def test_disallowed_grouping__must_be_position_only(self): dataset = (""" with_kwds From a417c5be3c570f859fc4ddd8690ed201854e0d79 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 28 Sep 2026 17:16:04 +0300 Subject: [PATCH 5/5] Reject a var-positional or var-keyword parameter in an optional group They were reported as an error on the following line, which was confusing: the closing ']' was blamed for following a var-keyword parameter. --- Lib/test/test_clinic.py | 25 ++++++++++--------------- Tools/clinic/libclinic/dsl_parser.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index cdfa4e2b71879f2..3b440fed7f6ce8a 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -2130,9 +2130,8 @@ def test_disallowed_grouping__parameter_after_group(self): # Only positional-only parameters can follow an optional group. group_err = ("You cannot use optional groups ('[' and ']') unless all " "parameters are positional-only ('/')") - kwds_err = ("Function 'bar' uses a var-keyword parameter and other " - "non-positional parameters, which Argument Clinic does " - "not currently support: '**kwds: dict'") + kwds_err = ("cannot use a var-keyword parameter with pos-or-keyword " + "or keyword-only parameters") dataset = ((""" module foo foo.bar @@ -2717,7 +2716,7 @@ def test_group_with_var_keyword(self): **kwds: dict ] """ - err = "parameters cannot follow var-keyword parameter: ']'" + err = "A var-keyword parameter cannot be in an optional group." self.expect_failure(block, err) def test_group_with_var_positional(self): @@ -2727,8 +2726,7 @@ def test_group_with_var_positional(self): *args: tuple ] """ - err = ("You cannot use optional groups ('[' and ']') unless all " - "parameters are positional-only ('/')") + err = "A var-positional parameter cannot be in an optional group." self.expect_failure(block, err) def test_depr_star_must_come_after_slash(self): @@ -3630,9 +3628,8 @@ def test_var_keyword_with_pos_or_kw(self): x: int **kwds: dict """ - err = ("Function 'bar' uses a var-keyword parameter and other " - "non-positional parameters, which Argument Clinic does " - "not currently support: '**kwds: dict'") + err = ("Function 'bar' cannot use a var-keyword parameter with " + "pos-or-keyword or keyword-only parameters.") self.expect_failure(block, err) def test_var_keyword_with_kw_only(self): @@ -3645,9 +3642,8 @@ def test_var_keyword_with_kw_only(self): y: int **kwds: dict """ - err = ("Function 'bar' uses a var-keyword parameter and other " - "non-positional parameters, which Argument Clinic does " - "not currently support: '**kwds: dict'") + err = ("Function 'bar' cannot use a var-keyword parameter with " + "pos-or-keyword or keyword-only parameters.") self.expect_failure(block, err) def test_var_keyword_with_pos_or_kw_and_kw_only(self): @@ -3661,9 +3657,8 @@ def test_var_keyword_with_pos_or_kw_and_kw_only(self): z: int **kwds: dict """ - err = ("Function 'bar' uses a var-keyword parameter and other " - "non-positional parameters, which Argument Clinic does " - "not currently support: '**kwds: dict'") + err = ("Function 'bar' cannot use a var-keyword parameter with " + "pos-or-keyword or keyword-only parameters.") self.expect_failure(block, err) def test_allow_negative_accepted_by_py_ssize_t_converter_only(self): diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index c7bf27b6e61aec1..514844af1d05b72 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -1020,9 +1020,9 @@ def parse_parameter(self, line: str) -> None: for p in self.function.parameters.values() ) if has_non_positional_param: - fail(f'Function {self.function.name!r} uses a var-keyword parameter ' - f'and other non-positional parameters, which Argument Clinic ' - f'does not currently support: {line!r}') + fail(f'Function {self.function.name!r} cannot use a var-keyword ' + f'parameter with pos-or-keyword or keyword-only ' + f'parameters.') is_var_keyword = True parameter = function_args.kwarg else: @@ -1161,6 +1161,9 @@ def bad_node(self, node: ast.AST) -> None: kind: inspect._ParameterKind if is_vararg: + if self.group_stack: + fail("A var-positional parameter cannot be in an optional " + "group.") if any(p.group for p in self.function.parameters.values()): # With "foo([a, b], *args)" the number of arguments does not # tell whether the group is passed or all arguments belong @@ -1169,6 +1172,9 @@ def bad_node(self, node: ast.AST) -> None: f"groups with a var-positional parameter.") kind = inspect.Parameter.VAR_POSITIONAL elif is_var_keyword: + if self.group_stack: + fail("A var-keyword parameter cannot be in an optional " + "group.") kind = inspect.Parameter.VAR_KEYWORD elif self.keyword_only: kind = inspect.Parameter.KEYWORD_ONLY