Skip to content

Commit a7f5d5f

Browse files
Prefer last same-named function in a class rather than first in igetattr() (#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 <[email protected]>
1 parent 3bcdcaf commit a7f5d5f

File tree

3 files changed

+63
-2
lines changed

3 files changed

+63
-2
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ What's New in astroid 3.2.0?
77
============================
88
Release date: TBA
99

10+
* ``igetattr()`` returns the last same-named function in a class (instead of
11+
the first). This avoids false positives in pylint with ``@overload``.
12+
13+
Closes #1015
14+
Refs pylint-dev/pylint#4696
1015

1116

1217
What's New in astroid 3.1.1?

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2508,12 +2508,21 @@ def igetattr(
25082508
# to the attribute happening *after* the attribute's definition (e.g. AugAssigns on lists)
25092509
if len(attributes) > 1:
25102510
first_attr, attributes = attributes[0], attributes[1:]
2511-
first_scope = first_attr.scope()
2511+
first_scope = first_attr.parent.scope()
25122512
attributes = [first_attr] + [
25132513
attr
25142514
for attr in attributes
25152515
if attr.parent and attr.parent.scope() == first_scope
25162516
]
2517+
functions = [attr for attr in attributes if isinstance(attr, FunctionDef)]
2518+
if functions:
2519+
# Prefer only the last function, unless a property is involved.
2520+
last_function = functions[-1]
2521+
attributes = [
2522+
a
2523+
for a in attributes
2524+
if a not in functions or a is last_function or bases._is_property(a)
2525+
]
25172526

25182527
for inferred in bases._infer_stmts(attributes, context, frame=self):
25192528
# yield Uninferable object instead of descriptors when necessary

tests/test_inference.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
)
3131
from astroid import decorators as decoratorsmod
3232
from astroid.arguments import CallSite
33-
from astroid.bases import BoundMethod, Instance, UnboundMethod, UnionType
33+
from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod, UnionType
3434
from astroid.builder import AstroidBuilder, _extract_single_node, extract_node, parse
3535
from astroid.const import IS_PYPY, PY39_PLUS, PY310_PLUS, PY312_PLUS
3636
from astroid.context import CallContext, InferenceContext
@@ -4321,6 +4321,53 @@ class Test(Outer.Inner):
43214321
assert isinstance(inferred, nodes.Const)
43224322
assert inferred.value == 123
43234323

4324+
def test_infer_method_empty_body(self) -> None:
4325+
# https://github.com/PyCQA/astroid/issues/1015
4326+
node = extract_node(
4327+
"""
4328+
class A:
4329+
def foo(self): ...
4330+
4331+
A().foo() #@
4332+
"""
4333+
)
4334+
inferred = next(node.infer())
4335+
assert isinstance(inferred, nodes.Const)
4336+
assert inferred.value is None
4337+
4338+
def test_infer_method_overload(self) -> None:
4339+
# https://github.com/PyCQA/astroid/issues/1015
4340+
node = extract_node(
4341+
"""
4342+
class A:
4343+
def foo(self): ...
4344+
4345+
def foo(self):
4346+
yield
4347+
4348+
A().foo() #@
4349+
"""
4350+
)
4351+
inferred = list(node.infer())
4352+
assert len(inferred) == 1
4353+
assert isinstance(inferred[0], Generator)
4354+
4355+
def test_infer_function_under_if(self) -> None:
4356+
node = extract_node(
4357+
"""
4358+
if 1 in [1]:
4359+
def func():
4360+
return 42
4361+
else:
4362+
def func():
4363+
return False
4364+
4365+
func() #@
4366+
"""
4367+
)
4368+
inferred = list(node.inferred())
4369+
assert [const.value for const in inferred] == [42, False]
4370+
43244371
def test_delayed_attributes_without_slots(self) -> None:
43254372
ast_node = extract_node(
43264373
"""

0 commit comments

Comments
 (0)