Skip to content

Commit

Permalink
use the ClassDef attribute inference process in Instances
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
temyurchenko committed Sep 13, 2024
1 parent a99967e commit f2c3947
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 50 deletions.
15 changes: 7 additions & 8 deletions astroid/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,27 +273,26 @@ 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
# descriptors
# 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)
Expand Down
85 changes: 44 additions & 41 deletions astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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__.
Expand Down
2 changes: 1 addition & 1 deletion tests/test_scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand Down

0 comments on commit f2c3947

Please sign in to comment.