From f2c3947233601f3b4ab6089ce49592f0ed64334a Mon Sep 17 00:00:00 2001 From: Artem Yurchenko Date: Wed, 11 Sep 2024 16:06:11 -0700 Subject: [PATCH] use the ClassDef attribute inference process in Instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instances often have «special_attributes». ClassDef does a lot of transformations of attibutes during inference, so let's reuse them in Instance. We are fixing inference of the "special" attributes of Instances. Before these changes, the FunctionDef attributes wouldn't get wrapped into a BoundMethod. This was facilitated by extracting the logic of inferring attributes into 'FunctionDef._infer_attrs' and reusing it in BaseInstance. This issue isn't visible right now, because the special attributes are simply not found due not being attached to the parent (the instance). Which in turn is caused by not providing the actual parent in the constructor. Since we are on a quest to always provide a parent, this change is necessary. --- astroid/bases.py | 15 ++-- astroid/nodes/scoped_nodes/scoped_nodes.py | 85 +++++++++++----------- tests/test_scoped_nodes.py | 2 +- 3 files changed, 52 insertions(+), 50 deletions(-) diff --git a/astroid/bases.py b/astroid/bases.py index 4a684cf1f..9f340cb16 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 629d524f9..7a9136dce 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 5e5bb581d..2b38eeecf 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 = """