Skip to content

Commit d392ea5

Browse files
Add new checker kwarg-superseded-by-positional-arg and fix a false positive (#8644)
* Fix a false positive for ``redundant-keyword-arg`` when a function, with a keyword-only-parameter and ``**kwargs``, is called with a positional argument and a keyword argument where the keyword argument has the same name as the positional-only-parameter. * Add new checker ``kwarg-superseded-by-positional-arg`` which emits a warning message for the above scenario. Closes #8558 Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent 0a6c21b commit d392ea5

12 files changed

+119
-25
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def print_name(name="Sarah", /, **kwds):
2+
print(name)
3+
4+
5+
print_name(name="Jacob") # [kwarg-superseded-by-positional-arg]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def print_name(name="Sarah", /, **kwds):
2+
print(name)
3+
4+
5+
print_name("Jacob")

doc/whatsnew/fragments/8558.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a new checker ``kwarg-superseded-by-positional-arg`` to warn when a function is called with a keyword argument which shares a name with a positional-only parameter and the function contains a keyword variadic parameter dictionary. It may be surprising behaviour when the keyword argument is added to the keyword variadic parameter dictionary.
2+
3+
Closes #8558

pylint/checkers/typecheck.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,13 @@ def _missing_member_hint(
399399
"isinstance-second-argument-not-valid-type",
400400
"Emitted when the second argument of an isinstance call is not a type.",
401401
),
402+
"W1117": (
403+
"%r will be included in %r since a positional-only parameter with this name already exists",
404+
"kwarg-superseded-by-positional-arg",
405+
"Emitted when a function is called with a keyword argument that has the "
406+
"same name as a positional-only parameter and the function contains a "
407+
"keyword variadic parameter dict.",
408+
),
402409
}
403410

404411
# builtin sequence types in Python 2 and 3.
@@ -1548,6 +1555,18 @@ def visit_call(self, node: nodes.Call) -> None:
15481555

15491556
# 2. Match the keyword arguments.
15501557
for keyword in keyword_args:
1558+
# Skip if `keyword` is the same name as a positional-only parameter
1559+
# and a `**kwargs` parameter exists.
1560+
if called.args.kwarg and keyword in [
1561+
arg.name for arg in called.args.posonlyargs
1562+
]:
1563+
self.add_message(
1564+
"kwarg-superseded-by-positional-arg",
1565+
node=node,
1566+
args=(keyword, f"**{called.args.kwarg}"),
1567+
confidence=HIGH,
1568+
)
1569+
continue
15511570
if keyword in parameter_name_to_index:
15521571
i = parameter_name_to_index[keyword]
15531572
if parameters[i][1]:
@@ -1564,11 +1583,6 @@ def visit_call(self, node: nodes.Call) -> None:
15641583
node=node,
15651584
args=(keyword, callable_name),
15661585
)
1567-
elif (
1568-
keyword in [arg.name for arg in called.args.posonlyargs]
1569-
and called.args.kwarg
1570-
):
1571-
pass
15721586
else:
15731587
parameters[i] = (parameters[i][0], True)
15741588
elif keyword in kwparams:

tests/functional/a/arguments.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# pylint: disable=too-few-public-methods, missing-docstring,import-error,wrong-import-position
22
# pylint: disable=wrong-import-order, unnecessary-lambda, consider-using-f-string
3-
# pylint: disable=unnecessary-lambda-assignment, no-self-argument, unused-argument
3+
# pylint: disable=unnecessary-lambda-assignment, no-self-argument, unused-argument, kwarg-superseded-by-positional-arg
44

55
def decorator(fun):
66
"""Decorator"""
@@ -278,3 +278,45 @@ def _print_selection(self):
278278
picker = FruitPicker()
279279
picker.pick_apple()
280280
picker.pick_pear()
281+
282+
283+
def name1(apple, /, **kwargs):
284+
"""
285+
Positional-only parameter with `**kwargs`.
286+
Calling this function with the `apple` keyword should not emit
287+
`redundant-keyword-arg` since it is added to `**kwargs`.
288+
289+
>>> name1("Red apple", apple="Green apple")
290+
"Red apple"
291+
{"apple": "Green apple"}
292+
"""
293+
print(apple)
294+
print(kwargs)
295+
296+
297+
name1("Red apple", apple="Green apple")
298+
299+
300+
def name2(apple, /, banana, **kwargs):
301+
"""
302+
Positional-only parameter with positional-or-keyword parameter and `**kwargs`.
303+
"""
304+
305+
306+
# `banana` is redundant
307+
# +1:[redundant-keyword-arg]
308+
name2("Red apple", "Yellow banana", apple="Green apple", banana="Green banana")
309+
310+
311+
# Test `no-value-for-parameter` in the context of positional-only parameters
312+
313+
def name3(param1, /, **kwargs): ...
314+
def name4(param1, /, param2, **kwargs): ...
315+
def name5(param1=True, /, **kwargs): ...
316+
def name6(param1, **kwargs): ...
317+
318+
name3(param1=43) # [no-value-for-parameter]
319+
name3(43)
320+
name4(1, param2=False)
321+
name5()
322+
name6(param1=43)

tests/functional/a/arguments.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ unexpected-keyword-arg:203:23:203:56:namedtuple_replace_issue_1036:Unexpected ke
3737
no-value-for-parameter:216:0:216:24::No value for argument 'third' in function call:UNDEFINED
3838
no-value-for-parameter:217:0:217:30::No value for argument 'second' in function call:UNDEFINED
3939
unexpected-keyword-arg:218:0:218:43::Unexpected keyword argument 'fourth' in function call:UNDEFINED
40+
redundant-keyword-arg:308:0:308:79::Argument 'banana' passed by position and keyword in function call:UNDEFINED
41+
no-value-for-parameter:318:0:318:16::No value for argument 'param1' in function call:UNDEFINED

tests/functional/a/arguments_positional_only.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/functional/a/arguments_positional_only.rc

Lines changed: 0 additions & 2 deletions
This file was deleted.

tests/functional/a/arguments_positional_only.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
The `kwarg-superseded-by-positional-arg` warning message is emitted when a function is called with
3+
a keyword argument which shares a name with a positional-only parameter and
4+
the function contains a keyword variadic parameter dictionary.
5+
It may be surprising behaviour when the keyword argument is added to the
6+
keyword variadic parameter dictionary.
7+
"""
8+
9+
10+
def name1(apple, /, banana="Yellow banana", **kwargs):
11+
"""
12+
Positional-only parameter, positional-or-keyword parameter and `**kwargs`.
13+
14+
>>> name1("Red apple", apple="Green apple", banana="Green banana")
15+
Red apple
16+
Green banana
17+
{"apple": "Green apple"}
18+
"""
19+
20+
print(apple)
21+
print(banana)
22+
print(kwargs)
23+
24+
25+
# +1: [kwarg-superseded-by-positional-arg]
26+
name1("Red apple", apple="Green apple", banana="Green banana")
27+
name1("Red apple", banana="Green banana")
28+
29+
30+
def name2(apple="Green apple", /, **kwargs):
31+
"""
32+
>>> name2(apple="Red apple")
33+
Green apple
34+
{'apple': 'Red apple'}
35+
"""
36+
print(apple)
37+
print(kwargs)
38+
39+
name2(apple="Red apple") # [kwarg-superseded-by-positional-arg]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
kwarg-superseded-by-positional-arg:26:0:26:62::'apple' will be included in '**kwargs' since a positional-only parameter with this name already exists:HIGH
2+
kwarg-superseded-by-positional-arg:39:0:39:24::'apple' will be included in '**kwargs' since a positional-only parameter with this name already exists:HIGH

tests/functional/p/positional_only_arguments_expected.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# pylint: disable=missing-docstring,unused-argument,pointless-statement
2-
# pylint: disable=too-few-public-methods
2+
# pylint: disable=too-few-public-methods, kwarg-superseded-by-positional-arg
33

44
class Gateaux:
55
def nihon(self, a, r, i, /, cheese=False):

0 commit comments

Comments
 (0)