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 pylint-dev#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 May 22, 2021
1 parent 4cfd9b6 commit c18286e
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 1 deletion.
3 changes: 2 additions & 1 deletion astroid/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,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 @@ -319,7 +320,7 @@ def infer_attribute(self, context=None):
):
pass
finally:
context.boundnode = None
context.boundnode = old_boundnode
return dict(node=self, context=context)


Expand Down
59 changes: 59 additions & 0 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -3894,6 +3894,65 @@ 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())
assert isinstance(inferred, nodes.Const)
assert 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())
assert isinstance(inferred, nodes.Const)
assert inferred.value == 123

def test_infer_subclass_attr_inner_class(self):
clsdef_node, attr_node = extract_node(
"""
class Outer:
class Inner:
data = 123
class Test(Outer.Inner):
pass
Test #@
Test.data #@
"""
)
clsdef = next(clsdef_node.infer())
assert isinstance(clsdef, nodes.ClassDef)
inferred = next(clsdef.igetattr("data"))
assert isinstance(inferred, nodes.Const)
assert inferred.value == 123
# Inferring the value of .data via igetattr() worked before the
# old_boundnode fixes in infer_subscript, so it should have been
# possible to infer the subscript directly. It is the difference
# between these two cases that led to the discovery of the cause of the
# bug in https://github.com/PyCQA/astroid/issues/904
inferred = next(attr_node.infer())
assert isinstance(inferred, nodes.Const)
assert inferred.value == 123

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

0 comments on commit c18286e

Please sign in to comment.