Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport maintenance/3.2.x] Fix FP for unexpected-keyword-arg with ambiguous constructors #9788

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/whatsnew/fragments/9672.false_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Quiet false positives for `unexpected-keyword-arg` when pylint cannot
determine which of two or more dynamically defined classes are being instantiated.

Closes #9672
2 changes: 1 addition & 1 deletion pylint/checkers/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,7 +1437,7 @@ def visit_call(self, node: nodes.Call) -> None:
"""Check that called functions/methods are inferred to callable objects,
and that passed arguments match the parameters in the inferred function.
"""
called = safe_infer(node.func)
called = safe_infer(node.func, compare_constructors=True)

self._check_not_callable(node, called)

Expand Down
26 changes: 26 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,7 @@
context: InferenceContext | None = None,
*,
compare_constants: bool = False,
compare_constructors: bool = False,
) -> InferenceResult | None:
"""Return the inferred value for the given node.

Expand All @@ -1354,6 +1355,9 @@

If compare_constants is True and if multiple constants are inferred,
unequal inferred values are also considered ambiguous and return None.

If compare_constructors is True and if multiple classes are inferred,
constructors with different signatures are held ambiguous and return None.
"""
inferred_types: set[str | None] = set()
try:
Expand Down Expand Up @@ -1386,6 +1390,13 @@
and function_arguments_are_ambiguous(inferred, value)
):
return None
if (
compare_constructors
and isinstance(inferred, nodes.ClassDef)
and isinstance(value, nodes.ClassDef)
and class_constructors_are_ambiguous(inferred, value)
):
return None
except astroid.InferenceError:
return None # There is some kind of ambiguity
except StopIteration:
Expand Down Expand Up @@ -1434,6 +1445,21 @@
return False


def class_constructors_are_ambiguous(
class1: nodes.ClassDef, class2: nodes.ClassDef
) -> bool:
try:
constructor1 = class1.local_attr("__init__")[0]
constructor2 = class2.local_attr("__init__")[0]
except astroid.NotFoundError:
return False

Check warning on line 1455 in pylint/checkers/utils.py

View check run for this annotation

Codecov / codecov/patch

pylint/checkers/utils.py#L1454-L1455

Added lines #L1454 - L1455 were not covered by tests
if not isinstance(constructor1, nodes.FunctionDef):
return False

Check warning on line 1457 in pylint/checkers/utils.py

View check run for this annotation

Codecov / codecov/patch

pylint/checkers/utils.py#L1457

Added line #L1457 was not covered by tests
if not isinstance(constructor2, nodes.FunctionDef):
return False

Check warning on line 1459 in pylint/checkers/utils.py

View check run for this annotation

Codecov / codecov/patch

pylint/checkers/utils.py#L1459

Added line #L1459 was not covered by tests
return function_arguments_are_ambiguous(constructor1, constructor2)


def has_known_bases(
klass: nodes.ClassDef, context: InferenceContext | None = None
) -> bool:
Expand Down
32 changes: 32 additions & 0 deletions tests/functional/u/unexpected_keyword_arg.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,35 @@ def ambiguous_func6(arg1=42):
# Two functions with same keyword argument but mixed defaults (names, constant)
func5 = ambiguous_func3 if unknown else ambiguous_func5
func5()


# pylint: disable=unused-argument
if do_something():
class AmbiguousClass:
def __init__(self, feeling="fine"):
...
else:
class AmbiguousClass:
def __init__(self, feeling="fine", thinking="hard"):
...


AmbiguousClass(feeling="so-so")
AmbiguousClass(thinking="carefully")
AmbiguousClass(worrying="little") # we could raise here if we infer_all()


if do_something():
class NotAmbiguousClass:
def __init__(self, feeling="fine"):
...
else:
class NotAmbiguousClass:
def __init__(self, feeling="fine"):
...


NotAmbiguousClass(feeling="so-so")
NotAmbiguousClass(worrying="little") # [unexpected-keyword-arg]

# pylint: enable=unused-argument
1 change: 1 addition & 0 deletions tests/functional/u/unexpected_keyword_arg.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ unexpected-keyword-arg:43:0:43:28::Unexpected keyword argument 'internal_arg' in
unexpected-keyword-arg:73:0:73:45::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
unexpected-keyword-arg:96:0:96:26::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
unexpected-keyword-arg:118:0:118:30::Unexpected keyword argument 'internal_arg' in function call:UNDEFINED
unexpected-keyword-arg:195:0:195:36::Unexpected keyword argument 'worrying' in constructor call:UNDEFINED
Loading