Skip to content

Commit 8eb2c4d

Browse files
Fix FP for unexpected-keyword-arg with ambiguous constructors (#9785) (#9788)
1 parent 9882537 commit 8eb2c4d

File tree

5 files changed

+64
-1
lines changed

5 files changed

+64
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Quiet false positives for `unexpected-keyword-arg` when pylint cannot
2+
determine which of two or more dynamically defined classes are being instantiated.
3+
4+
Closes #9672

pylint/checkers/typecheck.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1437,7 +1437,7 @@ def visit_call(self, node: nodes.Call) -> None:
14371437
"""Check that called functions/methods are inferred to callable objects,
14381438
and that passed arguments match the parameters in the inferred function.
14391439
"""
1440-
called = safe_infer(node.func)
1440+
called = safe_infer(node.func, compare_constructors=True)
14411441

14421442
self._check_not_callable(node, called)
14431443

pylint/checkers/utils.py

+26
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ def safe_infer(
13461346
context: InferenceContext | None = None,
13471347
*,
13481348
compare_constants: bool = False,
1349+
compare_constructors: bool = False,
13491350
) -> InferenceResult | None:
13501351
"""Return the inferred value for the given node.
13511352
@@ -1354,6 +1355,9 @@ def safe_infer(
13541355
13551356
If compare_constants is True and if multiple constants are inferred,
13561357
unequal inferred values are also considered ambiguous and return None.
1358+
1359+
If compare_constructors is True and if multiple classes are inferred,
1360+
constructors with different signatures are held ambiguous and return None.
13571361
"""
13581362
inferred_types: set[str | None] = set()
13591363
try:
@@ -1386,6 +1390,13 @@ def safe_infer(
13861390
and function_arguments_are_ambiguous(inferred, value)
13871391
):
13881392
return None
1393+
if (
1394+
compare_constructors
1395+
and isinstance(inferred, nodes.ClassDef)
1396+
and isinstance(value, nodes.ClassDef)
1397+
and class_constructors_are_ambiguous(inferred, value)
1398+
):
1399+
return None
13891400
except astroid.InferenceError:
13901401
return None # There is some kind of ambiguity
13911402
except StopIteration:
@@ -1434,6 +1445,21 @@ def function_arguments_are_ambiguous(
14341445
return False
14351446

14361447

1448+
def class_constructors_are_ambiguous(
1449+
class1: nodes.ClassDef, class2: nodes.ClassDef
1450+
) -> bool:
1451+
try:
1452+
constructor1 = class1.local_attr("__init__")[0]
1453+
constructor2 = class2.local_attr("__init__")[0]
1454+
except astroid.NotFoundError:
1455+
return False
1456+
if not isinstance(constructor1, nodes.FunctionDef):
1457+
return False
1458+
if not isinstance(constructor2, nodes.FunctionDef):
1459+
return False
1460+
return function_arguments_are_ambiguous(constructor1, constructor2)
1461+
1462+
14371463
def has_known_bases(
14381464
klass: nodes.ClassDef, context: InferenceContext | None = None
14391465
) -> bool:

tests/functional/u/unexpected_keyword_arg.py

+32
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,35 @@ def ambiguous_func6(arg1=42):
163163
# Two functions with same keyword argument but mixed defaults (names, constant)
164164
func5 = ambiguous_func3 if unknown else ambiguous_func5
165165
func5()
166+
167+
168+
# pylint: disable=unused-argument
169+
if do_something():
170+
class AmbiguousClass:
171+
def __init__(self, feeling="fine"):
172+
...
173+
else:
174+
class AmbiguousClass:
175+
def __init__(self, feeling="fine", thinking="hard"):
176+
...
177+
178+
179+
AmbiguousClass(feeling="so-so")
180+
AmbiguousClass(thinking="carefully")
181+
AmbiguousClass(worrying="little") # we could raise here if we infer_all()
182+
183+
184+
if do_something():
185+
class NotAmbiguousClass:
186+
def __init__(self, feeling="fine"):
187+
...
188+
else:
189+
class NotAmbiguousClass:
190+
def __init__(self, feeling="fine"):
191+
...
192+
193+
194+
NotAmbiguousClass(feeling="so-so")
195+
NotAmbiguousClass(worrying="little") # [unexpected-keyword-arg]
196+
197+
# pylint: enable=unused-argument

tests/functional/u/unexpected_keyword_arg.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ unexpected-keyword-arg:43:0:43:28::Unexpected keyword argument 'internal_arg' in
22
unexpected-keyword-arg:73:0:73:45::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
33
unexpected-keyword-arg:96:0:96:26::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
44
unexpected-keyword-arg:118:0:118:30::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
5+
unexpected-keyword-arg:195:0:195:36::Unexpected keyword argument 'worrying' in constructor call:UNDEFINED

0 commit comments

Comments
 (0)