Skip to content

Commit

Permalink
Prefer last same-named function in a class rather than first in `iget…
Browse files Browse the repository at this point in the history
…attr()` (#1173)

Ref #1015. When there are multiple statements defining some attribute
ClassDef.igetattr filters out later definitions if they are not in the
same scope as the first (to support cases like "self.a = 1; self.a = 2"
or "self.items = []; self.items += 1"). However, it checks the scope of
the first attribute against the *parent scope* of the later attributes.
For mundane statements this makes no difference, but for
scope-introducing statements such as FunctionDef these are not the same.

Fix this, and then filter to just last declared function (unless a property
is involved).

---------

Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
  • Loading branch information
nelfin and jacobtylerwalls authored Mar 8, 2024
1 parent 3bcdcaf commit a7f5d5f
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 2 deletions.
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
11 changes: 10 additions & 1 deletion astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 48 additions & 1 deletion tests/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
"""
Expand Down

0 comments on commit a7f5d5f

Please sign in to comment.