Skip to content

Commit bfeca4c

Browse files
authored
Check if decorator returns use keyword (unexpected-keyword-arg) (#5547)
* Improve coverage * Remove unnecessary declaration * Change spacing
1 parent bb9cb4b commit bfeca4c

File tree

5 files changed

+174
-0
lines changed

5 files changed

+174
-0
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ Release date: TBA
107107
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
108108
without these will trigger a ``DeprecationWarning``.
109109

110+
* Fixed false positive ``unexpected-keyword-arg`` for decorators.
111+
112+
Closes #258
113+
110114
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
111115

112116
.. code-block:: python

doc/whatsnew/2.13.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ Other Changes
101101
* The ``testutils`` for unittests now accept ``end_lineno`` and ``end_column``. Tests
102102
without these will trigger a ``DeprecationWarning``.
103103

104+
* Fixed false positive ``unexpected-keyword-arg`` for decorators.
105+
106+
Closes #258
107+
104108
* ``missing-raises-doc`` will now check the class hierarchy of the raised exceptions
105109

106110
.. code-block:: python

pylint/checkers/typecheck.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,10 @@ def visit_call(self, node: nodes.Call) -> None:
14571457
elif called.args.kwarg is not None:
14581458
# The keyword argument gets assigned to the **kwargs parameter.
14591459
pass
1460+
elif isinstance(
1461+
called, nodes.FunctionDef
1462+
) and self._keyword_argument_is_in_all_decorator_returns(called, keyword):
1463+
pass
14601464
elif not overload_function:
14611465
# Unexpected keyword argument.
14621466
self.add_message(
@@ -1496,6 +1500,46 @@ def visit_call(self, node: nodes.Call) -> None:
14961500
):
14971501
self.add_message("missing-kwoa", node=node, args=(name, callable_name))
14981502

1503+
@staticmethod
1504+
def _keyword_argument_is_in_all_decorator_returns(
1505+
func: nodes.FunctionDef, keyword: str
1506+
) -> bool:
1507+
"""Check if the keyword argument exists in all signatures of the
1508+
return values of all decorators of the function.
1509+
"""
1510+
if not func.decorators:
1511+
return False
1512+
1513+
for decorator in func.decorators.nodes:
1514+
inferred = safe_infer(decorator)
1515+
1516+
# If we can't infer the decorator we assume it satisfies consumes
1517+
# the keyword, so we don't raise false positives
1518+
if not inferred:
1519+
return True
1520+
1521+
# We only check arguments of function decorators
1522+
if not isinstance(inferred, nodes.FunctionDef):
1523+
return False
1524+
1525+
for return_value in inferred.infer_call_result():
1526+
# infer_call_result() returns nodes.Const.None for None return values
1527+
# so this also catches non-returning decorators
1528+
if not isinstance(return_value, nodes.FunctionDef):
1529+
return False
1530+
1531+
# If the return value uses a kwarg the keyword will be consumed
1532+
if return_value.args.kwarg:
1533+
continue
1534+
1535+
# Check if the keyword is another type of argument
1536+
if return_value.args.is_argument(keyword):
1537+
continue
1538+
1539+
return False
1540+
1541+
return True
1542+
14991543
def _check_invalid_sequence_index(self, subscript: nodes.Subscript):
15001544
# Look for index operations where the parent is a sequence type.
15011545
# If the types can be determined, only allow indices to be int,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Tests for unexpected-keyword-arg"""
2+
# pylint: disable=undefined-variable, too-few-public-methods, missing-function-docstring, missing-class-docstring
3+
4+
5+
def non_param_decorator(func):
6+
"""Decorator without a parameter"""
7+
8+
def new_func():
9+
func()
10+
11+
return new_func
12+
13+
14+
def param_decorator(func):
15+
"""Decorator with a parameter"""
16+
17+
def new_func(internal_arg=3):
18+
func(junk=internal_arg)
19+
20+
return new_func
21+
22+
23+
def kwargs_decorator(func):
24+
"""Decorator with kwargs.
25+
The if ... else makes the double decoration with param_decorator valid.
26+
"""
27+
28+
def new_func(**kwargs):
29+
if "internal_arg" in kwargs:
30+
func(junk=kwargs["internal_arg"])
31+
else:
32+
func(junk=kwargs["junk"])
33+
34+
return new_func
35+
36+
37+
@non_param_decorator
38+
def do_something(junk=None):
39+
"""A decorated function. This should not be passed a keyword argument"""
40+
print(junk)
41+
42+
43+
do_something(internal_arg=2) # [unexpected-keyword-arg]
44+
45+
46+
@param_decorator
47+
def do_something_decorated(junk=None):
48+
"""A decorated function. This should be passed a keyword argument"""
49+
print(junk)
50+
51+
52+
do_something_decorated(internal_arg=2)
53+
54+
55+
@kwargs_decorator
56+
def do_something_decorated_too(junk=None):
57+
"""A decorated function. This should be passed a keyword argument"""
58+
print(junk)
59+
60+
61+
do_something_decorated_too(internal_arg=2)
62+
63+
64+
@non_param_decorator
65+
@kwargs_decorator
66+
def do_something_double_decorated(junk=None):
67+
"""A decorated function. This should not be passed a keyword argument.
68+
non_param_decorator will raise an exception if a keyword argument is passed.
69+
"""
70+
print(junk)
71+
72+
73+
do_something_double_decorated(internal_arg=2) # [unexpected-keyword-arg]
74+
75+
76+
@param_decorator
77+
@kwargs_decorator
78+
def do_something_double_decorated_correct(junk=None):
79+
"""A decorated function. This should be passed a keyword argument"""
80+
print(junk)
81+
82+
83+
do_something_double_decorated_correct(internal_arg=2)
84+
85+
86+
# Test that we don't crash on Class decoration
87+
class DecoratorClass:
88+
pass
89+
90+
91+
@DecoratorClass
92+
def crash_test():
93+
pass
94+
95+
96+
crash_test(internal_arg=2) # [unexpected-keyword-arg]
97+
98+
99+
# Test that we don't emit a false positive for uninferable decorators
100+
@unknown_decorator
101+
def crash_test_two():
102+
pass
103+
104+
105+
crash_test_two(internal_arg=2)
106+
107+
108+
# Test that we don't crash on decorators that don't return anything
109+
def no_return_decorator(func):
110+
print(func)
111+
112+
113+
@no_return_decorator
114+
def test_no_return():
115+
pass
116+
117+
118+
test_no_return(internal_arg=2) # [unexpected-keyword-arg]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
unexpected-keyword-arg:43:0:43:28::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
2+
unexpected-keyword-arg:73:0:73:45::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
3+
unexpected-keyword-arg:96:0:96:26::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
4+
unexpected-keyword-arg:118:0:118:30::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED

0 commit comments

Comments
 (0)