diff --git a/doc/whatsnew/fragments/3613.false_positive b/doc/whatsnew/fragments/3613.false_positive new file mode 100644 index 0000000000..5a864208ad --- /dev/null +++ b/doc/whatsnew/fragments/3613.false_positive @@ -0,0 +1,6 @@ +Extend concept of "function ambiguity" in ``safe_infer()`` from +differing number of function arguments to differing set of argument names. + +Solves false positives in ``tensorflow``. + +Closes #3613 diff --git a/pylint/checkers/utils.py b/pylint/checkers/utils.py index f152bbacc1..08383a75f2 100644 --- a/pylint/checkers/utils.py +++ b/pylint/checkers/utils.py @@ -1381,12 +1381,10 @@ def safe_infer( return None if ( isinstance(inferred, nodes.FunctionDef) - and inferred.args.args is not None and isinstance(value, nodes.FunctionDef) - and value.args.args is not None - and len(inferred.args.args) != len(value.args.args) + and function_arguments_are_ambiguous(inferred, value) ): - return None # Different number of arguments indicates ambiguity + return None except astroid.InferenceError: return None # There is some kind of ambiguity except StopIteration: @@ -1408,6 +1406,33 @@ def infer_all( raise AstroidError from e +def function_arguments_are_ambiguous( + func1: nodes.FunctionDef, func2: nodes.FunctionDef +) -> bool: + if func1.argnames() != func2.argnames(): + return True + # Check ambiguity among function default values + pairs_of_defaults = [ + (func1.args.defaults, func2.args.defaults), + (func1.args.kw_defaults, func2.args.kw_defaults), + ] + for zippable_default in pairs_of_defaults: + if None in zippable_default: + continue + if len(zippable_default[0]) != len(zippable_default[1]): + return True + for default1, default2 in zip(*zippable_default): + if isinstance(default1, nodes.Const) and isinstance(default2, nodes.Const): + if default1.value != default2.value: + return True + elif isinstance(default1, nodes.Name) and isinstance(default2, nodes.Name): + if default1.name != default2.name: + return True + else: + return True + return False + + def has_known_bases( klass: nodes.ClassDef, context: InferenceContext | None = None ) -> bool: diff --git a/tests/functional/u/unexpected_keyword_arg.py b/tests/functional/u/unexpected_keyword_arg.py index e7b648899e..07b242ec49 100644 --- a/tests/functional/u/unexpected_keyword_arg.py +++ b/tests/functional/u/unexpected_keyword_arg.py @@ -116,3 +116,50 @@ def test_no_return(): test_no_return(internal_arg=2) # [unexpected-keyword-arg] + + +def ambiguous_func1(arg1): + print(arg1) + + +def ambiguous_func2(other_arg1): + print(other_arg1) + + +func1 = ambiguous_func1 if unknown else ambiguous_func2 +func1(other_arg1=1) + + +def ambiguous_func3(arg1=None): + print(arg1) + + +func2 = ambiguous_func1 if unknown else ambiguous_func3 +func2() + + +def ambiguous_func4(arg1=print): + print(arg1) + + +def ambiguous_func5(arg1=input): + print(arg1) + + +def ambiguous_func6(arg1=42): + print(arg1) + + +# Two functions with same keyword argument but different defaults (names) +func3 = ambiguous_func4 if unknown else ambiguous_func5 +func3() + + +# Two functions with same keyword argument but different defaults (constants) +func4 = ambiguous_func3 if unknown else ambiguous_func6 +func4() + + +# Two functions with same keyword argument but mixed defaults (names, constant) +func5 = ambiguous_func3 if unknown else ambiguous_func5 +func5()