diff --git a/astroid/bases.py b/astroid/bases.py index 4a684cf1f..f65cee47d 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -5,6 +5,8 @@ """This module contains base classes and functions for the nodes and some inference utils. """ +# isort: off + from __future__ import annotations import collections @@ -273,10 +275,9 @@ def igetattr( try: context.lookupname = name # XXX frame should be self._proxied, or not ? - get_attr = self.getattr(name, context, lookupclass=False) - yield from _infer_stmts( - self._wrap_attr(get_attr, context), context, frame=self - ) + attrs = self.getattr(name, context, lookupclass=False) + iattrs = self._proxied._infer_attrs(attrs, context, class_context=False) + yield from self._wrap_attr(iattrs) except AttributeInferenceError: try: # fallback to class.igetattr since it has some logic to handle @@ -284,16 +285,16 @@ def igetattr( # But only if the _proxied is the Class. if self._proxied.__class__.__name__ != "ClassDef": raise - attrs = self._proxied.igetattr(name, context, class_context=False) - yield from self._wrap_attr(attrs, context) + iattrs = self._proxied.igetattr(name, context, class_context=False) + yield from self._wrap_attr(iattrs, context) except AttributeInferenceError as error: raise InferenceError(**vars(error)) from error def _wrap_attr( - self, attrs: Iterable[InferenceResult], context: InferenceContext | None = None + self, iattrs: Iterable[InferenceResult], context: InferenceContext | None = None ) -> Iterator[InferenceResult]: """Wrap bound methods of attrs in a InstanceMethod proxies.""" - for attr in attrs: + for attr in iattrs: if isinstance(attr, UnboundMethod): if _is_property(attr): yield from attr.infer_call_result(self, context) diff --git a/astroid/brain/brain_argparse.py b/astroid/brain/brain_argparse.py index d0da4080a..bc9540c49 100644 --- a/astroid/brain/brain_argparse.py +++ b/astroid/brain/brain_argparse.py @@ -20,13 +20,10 @@ def infer_namespace(node, context: InferenceContext | None = None): "Namespace", lineno=node.lineno, col_offset=node.col_offset, - parent=nodes.Unknown(), + parent=AstroidManager().adhoc_module, # this class is not real end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, ) - # Set parent manually until ClassDef constructor fixed: - # https://github.com/pylint-dev/astroid/issues/1490 - class_node.parent = node.parent for attr in set(callsite.keyword_arguments): fake_node = nodes.EmptyNode() fake_node.parent = class_node diff --git a/astroid/brain/brain_builtin_inference.py b/astroid/brain/brain_builtin_inference.py index e9d00e2e1..6047103a4 100644 --- a/astroid/brain/brain_builtin_inference.py +++ b/astroid/brain/brain_builtin_inference.py @@ -23,6 +23,7 @@ ) from astroid.manager import AstroidManager from astroid.nodes import scoped_nodes +from astroid.raw_building import build_module from astroid.typing import ( ConstFactoryResult, InferenceResult, @@ -163,6 +164,8 @@ def _extend_builtins(class_transforms): def on_bootstrap(): """Called by astroid_bootstrapping().""" + AstroidManager().cache_module(build_module("__astroid_adhoc")) + _extend_builtins( { "bytes": partial(_extend_string_class, code=BYTES_CLASS, rvalue="b''"), diff --git a/astroid/brain/brain_namedtuple_enum.py b/astroid/brain/brain_namedtuple_enum.py index 71091d887..03a7d6924 100644 --- a/astroid/brain/brain_namedtuple_enum.py +++ b/astroid/brain/brain_namedtuple_enum.py @@ -73,7 +73,9 @@ def _extract_namedtuple_arg_or_keyword( # pylint: disable=inconsistent-return-s def infer_func_form( node: nodes.Call, - base_type: list[nodes.NodeNG], + base_type: nodes.NodeNG, + *, + parent: nodes.NodeNG, context: InferenceContext | None = None, enum: bool = False, ) -> tuple[nodes.ClassDef, str, list[str]]: @@ -146,15 +148,11 @@ def infer_func_form( col_offset=node.col_offset, end_lineno=node.end_lineno, end_col_offset=node.end_col_offset, - parent=nodes.Unknown(), + parent=parent, ) - # A typical ClassDef automatically adds its name to the parent scope, - # but doing so causes problems, so defer setting parent until after init - # see: https://github.com/pylint-dev/pylint/issues/5982 - class_node.parent = node.parent class_node.postinit( # set base class=tuple - bases=base_type, + bases=[base_type], body=[], decorators=None, ) @@ -194,25 +192,18 @@ def infer_named_tuple( node: nodes.Call, context: InferenceContext | None = None ) -> Iterator[nodes.ClassDef]: """Specific inference function for namedtuple Call node.""" - tuple_base_name: list[nodes.NodeNG] = [ - nodes.Name( - name="tuple", - parent=node.root(), - lineno=0, - col_offset=0, - end_lineno=None, - end_col_offset=None, - ) - ] + tuple_base = util.safe_infer(_extract_single_node("tuple")) + assert isinstance(tuple_base, nodes.NodeNG) + class_node, name, attributes = infer_func_form( - node, tuple_base_name, context=context + node, tuple_base, parent=AstroidManager().adhoc_module, context=context ) + call_site = arguments.CallSite.from_call(node, context=context) - node = extract_node("import collections; collections.namedtuple") - try: - func = next(node.infer()) - except StopIteration as e: - raise InferenceError(node=node) from e + func = util.safe_infer( + _extract_single_node("import collections; collections.namedtuple") + ) + assert isinstance(func, nodes.NodeNG) try: rename = next( call_site.infer_argument(func, "rename", context or InferenceContext()) @@ -363,7 +354,17 @@ def value(self): __members__ = [''] """ ) - class_node = infer_func_form(node, [enum_meta], context=context, enum=True)[0] + + # FIXME arguably, the base here shouldn't be the EnumMeta class definition + # itself, but a reference (Name) to it. Otherwise, the invariant that all + # children of a node have that node as their parent is broken. + class_node = infer_func_form( + node, + enum_meta, + parent=AstroidManager().adhoc_module, + context=context, + enum=True, + )[0] return iter([class_node.instantiate_class()]) diff --git a/astroid/helpers.py b/astroid/helpers.py index fe57b16bb..de5f44c10 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -37,8 +37,7 @@ def safe_infer( def _build_proxy_class(cls_name: str, builtins: nodes.Module) -> nodes.ClassDef: - proxy = raw_building.build_class(cls_name) - proxy.parent = builtins + proxy = raw_building.build_class(cls_name, builtins) return proxy diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py index 0f553ab08..69babb96a 100644 --- a/astroid/interpreter/objectmodel.py +++ b/astroid/interpreter/objectmodel.py @@ -21,6 +21,8 @@ mechanism. """ +# isort: off + from __future__ import annotations import itertools @@ -493,7 +495,7 @@ def __init__(self): super().__init__() @property - def attr___annotations__(self) -> node_classes.Unkown: + def attr___annotations__(self) -> node_classes.Unknown: return node_classes.Unknown() @property diff --git a/astroid/manager.py b/astroid/manager.py index 7206be616..7ed4525b8 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -115,6 +115,10 @@ def unregister_transform(self): def builtins_module(self) -> nodes.Module: return self.astroid_cache["builtins"] + @property + def adhoc_module(self) -> nodes.Module: + return self.astroid_cache["__astroid_adhoc"] + @property def prefer_stubs(self) -> bool: return AstroidManager.brain["prefer_stubs"] diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 2f7e8001b..3f00ec3cd 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -8,6 +8,8 @@ Lambda, GeneratorExp, DictComp and SetComp to some extent). """ +# isort: skip_file + from __future__ import annotations import io @@ -44,7 +46,6 @@ Arguments, Const, NodeNG, - Unknown, _base_nodes, const_factory, node_classes, @@ -1166,9 +1167,11 @@ def __init__( end_col_offset=end_col_offset, parent=parent, ) - if parent and not isinstance(parent, Unknown): + if parent: frame = parent.frame() frame.set_local(name, self) + if frame is parent: + frame._append_node(self) def postinit( self, @@ -1935,7 +1938,7 @@ def __init__( end_col_offset=end_col_offset, parent=parent, ) - if parent and not isinstance(parent, Unknown): + if parent: parent.frame().set_local(name, self) for local_name, node in self.implicit_locals(): @@ -2089,7 +2092,7 @@ def _infer_type_call(self, caller, context): col_offset=0, end_lineno=0, end_col_offset=0, - parent=Unknown(), + parent=caller.parent, ) # Get the bases of the class. @@ -2123,7 +2126,6 @@ def _infer_type_call(self, caller, context): if isinstance(attr, node_classes.Const) and isinstance(attr.value, str): result.locals[attr.value] = [value] - result.parent = caller.parent return result def infer_call_result( @@ -2486,14 +2488,11 @@ def igetattr( :returns: The inferred possible values. """ - from astroid import objects # pylint: disable=import-outside-toplevel - # set lookup name since this is necessary to infer on import nodes for # instance context = copy_context(context) context.lookupname = name - metaclass = self.metaclass(context=context) try: attributes = self.getattr(name, context, class_context=class_context) # If we have more than one attribute, make sure that those starting from @@ -2516,44 +2515,7 @@ def igetattr( 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 - if not isinstance(inferred, node_classes.Const) and isinstance( - inferred, bases.Instance - ): - try: - inferred._proxied.getattr("__get__", context) - except AttributeInferenceError: - yield inferred - else: - yield util.Uninferable - elif isinstance(inferred, objects.Property): - function = inferred.function - if not class_context: - if not context.callcontext: - context.callcontext = CallContext( - args=function.args.arguments, callee=function - ) - # Through an instance so we can solve the property - yield from function.infer_call_result( - caller=self, context=context - ) - # If we're in a class context, we need to determine if the property - # was defined in the metaclass (a derived class must be a subclass of - # the metaclass of all its bases), in which case we can resolve the - # property. If not, i.e. the property is defined in some base class - # instead, then we return the property object - elif metaclass and function.parent.scope() is metaclass: - # Resolve a property as long as it is not accessed through - # the class itself. - yield from function.infer_call_result( - caller=self, context=context - ) - else: - yield inferred - else: - yield function_to_method(inferred, self) + yield from self._infer_attrs(attributes, context, class_context) except AttributeInferenceError as error: if not name.startswith("__") and self.has_dynamic_getattr(context): # class handle some dynamic attributes, return a Uninferable object @@ -2563,6 +2525,49 @@ def igetattr( str(error), target=self, attribute=name, context=context ) from error + def _infer_attrs( + self, + attributes: list[InferenceResult], + context: InferenceContext, + class_context: bool = True, + ) -> Iterator[InferenceResult]: + from astroid import objects # pylint: disable=import-outside-toplevel + + metaclass = self.metaclass(context=context) + for inferred in bases._infer_stmts(attributes, context, frame=self): + # yield Uninferable object instead of descriptors when necessary + if not isinstance(inferred, node_classes.Const) and isinstance( + inferred, bases.Instance + ): + try: + inferred._proxied.getattr("__get__", context) + except AttributeInferenceError: + yield inferred + else: + yield util.Uninferable + elif isinstance(inferred, objects.Property): + function = inferred.function + if not class_context: + if not context.callcontext: + context.callcontext = CallContext( + args=function.args.arguments, callee=function + ) + # Through an instance so we can solve the property + yield from function.infer_call_result(caller=self, context=context) + # If we're in a class context, we need to determine if the property + # was defined in the metaclass (a derived class must be a subclass of + # the metaclass of all its bases), in which case we can resolve the + # property. If not, i.e. the property is defined in some base class + # instead, then we return the property object + elif metaclass and function.parent.scope() is metaclass: + # Resolve a property as long as it is not accessed through + # the class itself. + yield from function.infer_call_result(caller=self, context=context) + else: + yield inferred + else: + yield function_to_method(inferred, self) + def has_dynamic_getattr(self, context: InferenceContext | None = None) -> bool: """Check if the class has a custom __getattr__ or __getattribute__. diff --git a/astroid/objects.py b/astroid/objects.py index 2d12f5980..cf98e8422 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -282,7 +282,7 @@ def __init__(self, call, name=None, lineno=None, col_offset=None, parent=None): name, lineno=lineno, col_offset=col_offset, - parent=node_classes.Unknown(), + parent=AstroidManager().adhoc_module, end_col_offset=0, end_lineno=0, ) diff --git a/astroid/raw_building.py b/astroid/raw_building.py index a89a87b57..1a65c83f5 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -49,14 +49,14 @@ def _attach_local_node(parent, node, name: str) -> None: parent.add_local_node(node) -def _add_dunder_class(func, member) -> None: +def _add_dunder_class(func, parent: nodes.NodeNG, member) -> None: """Add a __class__ member to the given func node, if we can determine it.""" python_cls = member.__class__ cls_name = getattr(python_cls, "__name__", None) if not cls_name: return cls_bases = [ancestor.__name__ for ancestor in python_cls.__bases__] - ast_klass = build_class(cls_name, cls_bases, python_cls.__doc__) + ast_klass = build_class(cls_name, parent, cls_bases, python_cls.__doc__) func.instance_attrs["__class__"] = [ast_klass] @@ -96,7 +96,10 @@ def build_module(name: str, doc: str | None = None) -> nodes.Module: def build_class( - name: str, basenames: Iterable[str] = (), doc: str | None = None + name: str, + parent: nodes.NodeNG, + basenames: Iterable[str] = (), + doc: str | None = None, ) -> nodes.ClassDef: """Create and initialize an astroid ClassDef node.""" node = nodes.ClassDef( @@ -105,7 +108,7 @@ def build_class( col_offset=0, end_lineno=0, end_col_offset=0, - parent=nodes.Unknown(), + parent=parent, ) node.postinit( bases=[ @@ -128,6 +131,7 @@ def build_class( def build_function( name: str, + parent: nodes.NodeNG, args: list[str] | None = None, posonlyargs: list[str] | None = None, defaults: list[Any] | None = None, @@ -141,7 +145,7 @@ def build_function( name, lineno=0, col_offset=0, - parent=node_classes.Unknown(), + parent=parent, end_col_offset=0, end_lineno=0, ) @@ -311,8 +315,9 @@ def object_build_function( kwonly_defaults, ) = _get_args_info_from_callable(member) - func = build_function( + build_function( getattr(member, "__name__", None) or localname, + node, args, posonlyargs, defaults, @@ -321,8 +326,6 @@ def object_build_function( kwonlydefaults=kwonly_defaults, ) - node.add_local_node(func, localname) - def object_build_datadescriptor( node: nodes.Module | nodes.ClassDef, member: type, name: str @@ -339,10 +342,9 @@ def object_build_methoddescriptor( """create astroid for a living method descriptor object""" # FIXME get arguments ? func = build_function( - getattr(member, "__name__", None) or localname, doc=member.__doc__ + getattr(member, "__name__", None) or localname, node, doc=member.__doc__ ) - node.add_local_node(func, localname) - _add_dunder_class(func, member) + _add_dunder_class(func, node, member) def _base_class_object_build( @@ -359,11 +361,11 @@ def _base_class_object_build( assert isinstance(class_name, str) klass = build_class( class_name, + node, basenames, member.__doc__, ) klass._newstyle = isinstance(member, type) - node.add_local_node(klass, localname) try: # limit the instantiation trick since it's too dangerous # (such as infinite test execution...) @@ -605,14 +607,11 @@ def _astroid_bootstrapping() -> None: for cls, node_cls in node_classes.CONST_CLS.items(): if cls is TYPE_NONE: - proxy = build_class("NoneType") - proxy.parent = astroid_builtin + proxy = build_class("NoneType", astroid_builtin) elif cls is TYPE_NOTIMPLEMENTED: - proxy = build_class("NotImplementedType") - proxy.parent = astroid_builtin + proxy = build_class("NotImplementedType", astroid_builtin) elif cls is TYPE_ELLIPSIS: - proxy = build_class("Ellipsis") - proxy.parent = astroid_builtin + proxy = build_class("Ellipsis", astroid_builtin) else: proxy = astroid_builtin.getattr(cls.__name__)[0] assert isinstance(proxy, nodes.ClassDef) @@ -630,9 +629,8 @@ def _astroid_bootstrapping() -> None: col_offset=0, end_lineno=0, end_col_offset=0, - parent=nodes.Unknown(), + parent=astroid_builtin, ) - _GeneratorType.parent = astroid_builtin generator_doc_node = ( nodes.Const(value=types.GeneratorType.__doc__) if types.GeneratorType.__doc__ @@ -654,9 +652,8 @@ def _astroid_bootstrapping() -> None: col_offset=0, end_lineno=0, end_col_offset=0, - parent=nodes.Unknown(), + parent=astroid_builtin, ) - _AsyncGeneratorType.parent = astroid_builtin async_generator_doc_node = ( nodes.Const(value=types.AsyncGeneratorType.__doc__) if types.AsyncGeneratorType.__doc__ @@ -678,9 +675,8 @@ def _astroid_bootstrapping() -> None: col_offset=0, end_lineno=0, end_col_offset=0, - parent=nodes.Unknown(), + parent=astroid_builtin, ) - _UnionTypeType.parent = astroid_builtin union_type_doc_node = ( nodes.Const(value=types.UnionType.__doc__) if types.UnionType.__doc__ @@ -715,9 +711,8 @@ def _astroid_bootstrapping() -> None: col_offset=0, end_lineno=0, end_col_offset=0, - parent=nodes.Unknown(), + parent=astroid_builtin, ) - klass.parent = astroid_builtin klass.postinit( bases=[], body=[], diff --git a/astroid/transforms.py b/astroid/transforms.py index 1831e74b5..efb5d469b 100644 --- a/astroid/transforms.py +++ b/astroid/transforms.py @@ -9,11 +9,11 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Optional, TypeVar, Union, cast, overload +from astroid import nodes from astroid.context import _invalidate_cache from astroid.typing import SuccessfulInferenceResult, TransformFn if TYPE_CHECKING: - from astroid import nodes _SuccessfulInferenceResultT = TypeVar( "_SuccessfulInferenceResultT", bound=SuccessfulInferenceResult diff --git a/tests/brain/test_brain.py b/tests/brain/test_brain.py index 447c4cde2..14d4bd5d8 100644 --- a/tests/brain/test_brain.py +++ b/tests/brain/test_brain.py @@ -1705,7 +1705,8 @@ def test_infer_dict_from_keys() -> None: ) for node in bad_nodes: with pytest.raises(InferenceError): - next(node.infer()) + if isinstance(next(node.infer()), util.UninferableBase): + raise InferenceError # Test uninferable values good_nodes = astroid.extract_node( diff --git a/tests/brain/test_named_tuple.py b/tests/brain/test_named_tuple.py index 40a96c7ce..c802982e7 100644 --- a/tests/brain/test_named_tuple.py +++ b/tests/brain/test_named_tuple.py @@ -175,19 +175,6 @@ def test_namedtuple_func_form_args_and_kwargs(self) -> None: self.assertIn("b", inferred.locals) self.assertIn("c", inferred.locals) - def test_namedtuple_bases_are_actually_names_not_nodes(self) -> None: - node = builder.extract_node( - """ - from collections import namedtuple - Tuple = namedtuple("Tuple", field_names="a b c", rename=UNINFERABLE) - Tuple #@ - """ - ) - inferred = next(node.infer()) - self.assertIsInstance(inferred, astroid.ClassDef) - self.assertIsInstance(inferred.bases[0], astroid.Name) - self.assertEqual(inferred.bases[0].name, "tuple") - def test_invalid_label_does_not_crash_inference(self) -> None: code = """ import collections diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 2dd94a6ae..170176f93 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -26,8 +26,7 @@ def _extract(self, obj_name: str) -> ClassDef: return self.builtins.getattr(obj_name)[0] def _build_custom_builtin(self, obj_name: str) -> ClassDef: - proxy = raw_building.build_class(obj_name) - proxy.parent = self.builtins + proxy = raw_building.build_class(obj_name, self.builtins) return proxy def assert_classes_equal(self, cls: ClassDef, other: ClassDef) -> None: diff --git a/tests/test_nodes.py b/tests/test_nodes.py index 64cae2f67..5ec66ce58 100644 --- a/tests/test_nodes.py +++ b/tests/test_nodes.py @@ -37,6 +37,7 @@ ParentMissingError, StatementMissing, ) +from astroid.manager import AstroidManager from astroid.nodes.node_classes import ( AssignAttr, AssignName, @@ -1973,7 +1974,9 @@ def test_str_repr_no_warnings(node): if name == "self": continue - if "int" in param_type.annotation: + if name == "parent" and "NodeNG" in param_type.annotation: + args[name] = AstroidManager().adhoc_module + elif "int" in param_type.annotation: args[name] = random.randint(0, 50) elif ( "NodeNG" in param_type.annotation diff --git a/tests/test_object_model.py b/tests/test_object_model.py index 9ad4d39a9..49ac91408 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -2,6 +2,8 @@ # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt +# isort: skip_file + import unittest import xml diff --git a/tests/test_raw_building.py b/tests/test_raw_building.py index 951bf09d9..b5f3a6262 100644 --- a/tests/test_raw_building.py +++ b/tests/test_raw_building.py @@ -33,6 +33,8 @@ build_module, ) +DUMMY_MOD = build_module("DUMMY") + class RawBuildingTC(unittest.TestCase): def test_attach_dummy_node(self) -> None: @@ -48,33 +50,33 @@ def test_build_module(self) -> None: self.assertEqual(node.parent, None) def test_build_class(self) -> None: - node = build_class("MyClass") + node = build_class("MyClass", DUMMY_MOD) self.assertEqual(node.name, "MyClass") self.assertEqual(node.doc_node, None) def test_build_function(self) -> None: - node = build_function("MyFunction") + node = build_function("MyFunction", DUMMY_MOD) self.assertEqual(node.name, "MyFunction") self.assertEqual(node.doc_node, None) def test_build_function_args(self) -> None: args = ["myArgs1", "myArgs2"] - node = build_function("MyFunction", args) + node = build_function("MyFunction", DUMMY_MOD, args) self.assertEqual("myArgs1", node.args.args[0].name) self.assertEqual("myArgs2", node.args.args[1].name) self.assertEqual(2, len(node.args.args)) def test_build_function_defaults(self) -> None: defaults = ["defaults1", "defaults2"] - node = build_function(name="MyFunction", args=None, defaults=defaults) + node = build_function("MyFunction", DUMMY_MOD, args=None, defaults=defaults) self.assertEqual(2, len(node.args.defaults)) def test_build_function_posonlyargs(self) -> None: - node = build_function(name="MyFunction", posonlyargs=["a", "b"]) + node = build_function("MyFunction", DUMMY_MOD, posonlyargs=["a", "b"]) self.assertEqual(2, len(node.args.posonlyargs)) def test_build_function_kwonlyargs(self) -> None: - node = build_function(name="MyFunction", kwonlyargs=["a", "b"]) + node = build_function("MyFunction", DUMMY_MOD, kwonlyargs=["a", "b"]) assert len(node.args.kwonlyargs) == 2 assert node.args.kwonlyargs[0].name == "a" assert node.args.kwonlyargs[1].name == "b" diff --git a/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py index 209710b86..225f2c289 100644 --- a/tests/test_scoped_nodes.py +++ b/tests/test_scoped_nodes.py @@ -1296,7 +1296,7 @@ def func(arg1, arg2): self.assertIsInstance(inferred[0], BoundMethod) inferred = list(Instance(cls).igetattr("m4")) self.assertEqual(len(inferred), 1) - self.assertIsInstance(inferred[0], nodes.FunctionDef) + self.assertIsInstance(inferred[0], BoundMethod) def test_getattr_from_grandpa(self) -> None: data = """