diff --git a/astroid/bases.py b/astroid/bases.py index 4a684cf1fe..9f340cb16e 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -273,10 +273,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 +283,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/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 629d524f9f..7a9136dcec 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -2465,14 +2465,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 @@ -2495,44 +2492,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 @@ -2542,6 +2502,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/tests/test_scoped_nodes.py b/tests/test_scoped_nodes.py index 5e5bb581d4..2b38eeecf7 100644 --- a/tests/test_scoped_nodes.py +++ b/tests/test_scoped_nodes.py @@ -1295,7 +1295,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 = """