Skip to content

Commit

Permalink
Fix inference cycle in getattr when a base is an astroid.Attribute
Browse files Browse the repository at this point in the history
Ref #904. `getattr` attempts to find values on `self.ancestors`, which
infers each node listed in `self.bases`. Removing or resetting the
context passed to ancestors allows it to infer the nodes, but opens up
the possibility of cycles through recursive definitions. We should be
able to infer the value of the base node correctly as a `ClassDef`. The
root of this issue stems from the fact that `infer_attribute` attempts
to temporarily set `context.boundnode`, but when unwrapping its changes
it sets the `boundnode` to `None` instead of its previous value, which
loses state and causes the inference of the `Attribute` node to have a
duplicate key: once when looking up the class definition (which should
have `boundnode = None`), and once when inferring the (other) attribute
value (which should not have `boundnode = None`)
  • Loading branch information
nelfin committed Apr 7, 2021
1 parent 31a731a commit eb2e14d
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 1 deletion.
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Release Date: TBA

* Use ``inference_tip`` for ``typing.TypedDict`` brain.

* Fix inference of attributes defined in a base class that is an inner class

Closes #904

What's New in astroid 2.5.2?
============================
Expand Down
3 changes: 2 additions & 1 deletion astroid/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ def infer_attribute(self, context=None):
elif not context:
context = contextmod.InferenceContext()

old_boundnode = context.boundnode
try:
context.boundnode = owner
yield from owner.igetattr(self.attrname, context)
Expand All @@ -325,7 +326,7 @@ def infer_attribute(self, context=None):
):
pass
finally:
context.boundnode = None
context.boundnode = old_boundnode
return dict(node=self, context=context)


Expand Down
66 changes: 66 additions & 0 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -3890,6 +3890,72 @@ class Clazz(metaclass=_Meta):
).inferred()[0]
assert isinstance(cls, nodes.ClassDef) and cls.name == "Clazz"

def test_infer_subclass_attr_outer_class(self):
node = extract_node(
"""
class Outer:
data = 123
class Test(Outer):
pass
Test.data
"""
)
inferred = next(node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 123)

def test_infer_subclass_attr_inner_class_works_from_classdef(self):
node = extract_node(
"""
class Outer:
class Inner:
data = 123
class Test(Outer.Inner):
pass
Test
"""
)
cls_def = next(node.infer())
self.assertIsInstance(cls_def, nodes.ClassDef)
inferred = next(cls_def.igetattr('data'))
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 123)

def test_infer_subclass_attr_inner_class_works_indirectly(self):
node = extract_node(
"""
class Outer:
class Inner:
data = 123
Inner = Outer.Inner
class Test(Inner):
pass
Test.data
"""
)
inferred = next(node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 123)

def test_infer_subclass_attr_inner_class(self):
node = extract_node(
"""
class Outer:
class Inner:
data = 123
class Test(Outer.Inner):
pass
Test.data
"""
)
inferred = next(node.infer())
self.assertIsInstance(inferred, nodes.Const)
self.assertEqual(inferred.value, 123)

def test_delayed_attributes_without_slots(self):
ast_node = extract_node(
"""
Expand Down

0 comments on commit eb2e14d

Please sign in to comment.