Skip to content
96 changes: 70 additions & 26 deletions Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,28 +379,26 @@ 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'"
def test_parameter_after_var_keyword(self):
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):
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
**
**kwds: dict
[clinic start generated code]*/
"""
self.expect_failure(block, err, lineno=5)
Expand Down Expand Up @@ -2128,6 +2126,42 @@ def test_disallowed_grouping__varpos(self):
"""
self.expect_failure(block, err, lineno=4)

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 = ("cannot use a var-keyword parameter with pos-or-keyword "
"or keyword-only parameters")
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
]
**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
Expand All @@ -2140,11 +2174,6 @@ def test_disallowed_grouping__must_be_position_only(self):
[
a: object
]
""", """
with_kwds
[
**kwds: dict
]
Comment thread
serhiy-storchaka marked this conversation as resolved.
""")
err = (
"You cannot use optional groups ('[' and ']') unless all "
Expand Down Expand Up @@ -2654,38 +2683,50 @@ 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 = "A var-keyword parameter cannot be in an optional group."
self.expect_failure(block, err)

def test_group_with_var_positional(self):
block = """
with_varpos
[
*args: tuple
]
"""
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):
Expand Down Expand Up @@ -2777,7 +2818,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
Expand Down Expand Up @@ -3587,7 +3628,8 @@ 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' 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):
Expand All @@ -3600,7 +3642,8 @@ 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' 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):
Expand All @@ -3614,7 +3657,8 @@ 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' 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):
Expand Down
24 changes: 12 additions & 12 deletions Tools/clinic/libclinic/dsl_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,9 @@ def state_parameter(self, line: str) -> None:
self.deprecated_until = self.parse_version(match[1], 'until')
line = match[2]

if not self.expecting_parameters:
fail(f'parameters cannot follow var-keyword parameter: {line!r}')

func = self.function
match line:
case '*':
Expand All @@ -967,10 +970,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()
Expand Down Expand Up @@ -1021,8 +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} has an "
f"invalid parameter declaration (**kwargs?): {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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -1287,9 +1293,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()
Expand Down Expand Up @@ -1356,9 +1359,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 ...]'")
Expand Down
Loading