diff --git a/ChangeLog b/ChangeLog index 86b614c1f6..ac5df56f4f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,11 @@ What's New in astroid 3.2.0? ============================ Release date: TBA +* ``igetattr()`` returns the last same-named function in a class (instead of + the first). This avoids false positives in pylint with ``@overload``. + + Closes #1015 + Refs pylint-dev/pylint#4696 What's New in astroid 3.1.1? diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 9cda4f1be0..79b7643e55 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2508,12 +2508,21 @@ def igetattr( # to the attribute happening *after* the attribute's definition (e.g. AugAssigns on lists) if len(attributes) > 1: first_attr, attributes = attributes[0], attributes[1:] - first_scope = first_attr.scope() + first_scope = first_attr.parent.scope() attributes = [first_attr] + [ attr for attr in attributes if attr.parent and attr.parent.scope() == first_scope ] + functions = [attr for attr in attributes if isinstance(attr, FunctionDef)] + if functions: + # Prefer only the last function, unless a property is involved. + last_function = functions[-1] + attributes = [ + a + for a in attributes + if a not in functions or a is last_function or bases._is_property(a) + ] for inferred in bases._infer_stmts(attributes, context, frame=self): # yield Uninferable object instead of descriptors when necessary diff --git a/tests/test_inference.py b/tests/test_inference.py index ffd78fe035..10fceb7b56 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -30,7 +30,7 @@ ) from astroid import decorators as decoratorsmod from astroid.arguments import CallSite -from astroid.bases import BoundMethod, Instance, UnboundMethod, UnionType +from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod, UnionType from astroid.builder import AstroidBuilder, _extract_single_node, extract_node, parse from astroid.const import IS_PYPY, PY39_PLUS, PY310_PLUS, PY312_PLUS from astroid.context import CallContext, InferenceContext @@ -4321,6 +4321,53 @@ class Test(Outer.Inner): assert isinstance(inferred, nodes.Const) assert inferred.value == 123 + def test_infer_method_empty_body(self) -> None: + # https://github.com/PyCQA/astroid/issues/1015 + node = extract_node( + """ + class A: + def foo(self): ... + + A().foo() #@ + """ + ) + inferred = next(node.infer()) + assert isinstance(inferred, nodes.Const) + assert inferred.value is None + + def test_infer_method_overload(self) -> None: + # https://github.com/PyCQA/astroid/issues/1015 + node = extract_node( + """ + class A: + def foo(self): ... + + def foo(self): + yield + + A().foo() #@ + """ + ) + inferred = list(node.infer()) + assert len(inferred) == 1 + assert isinstance(inferred[0], Generator) + + def test_infer_function_under_if(self) -> None: + node = extract_node( + """ + if 1 in [1]: + def func(): + return 42 + else: + def func(): + return False + + func() #@ + """ + ) + inferred = list(node.inferred()) + assert [const.value for const in inferred] == [42, False] + def test_delayed_attributes_without_slots(self) -> None: ast_node = extract_node( """