Skip to content

Commit f2c3947

Browse files
committed
use the ClassDef attribute inference process in Instances
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.
1 parent a99967e commit f2c3947

File tree

3 files changed

+52
-50
lines changed

3 files changed

+52
-50
lines changed

astroid/bases.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,27 +273,26 @@ def igetattr(
273273
try:
274274
context.lookupname = name
275275
# XXX frame should be self._proxied, or not ?
276-
get_attr = self.getattr(name, context, lookupclass=False)
277-
yield from _infer_stmts(
278-
self._wrap_attr(get_attr, context), context, frame=self
279-
)
276+
attrs = self.getattr(name, context, lookupclass=False)
277+
iattrs = self._proxied._infer_attrs(attrs, context, class_context=False)
278+
yield from self._wrap_attr(iattrs)
280279
except AttributeInferenceError:
281280
try:
282281
# fallback to class.igetattr since it has some logic to handle
283282
# descriptors
284283
# But only if the _proxied is the Class.
285284
if self._proxied.__class__.__name__ != "ClassDef":
286285
raise
287-
attrs = self._proxied.igetattr(name, context, class_context=False)
288-
yield from self._wrap_attr(attrs, context)
286+
iattrs = self._proxied.igetattr(name, context, class_context=False)
287+
yield from self._wrap_attr(iattrs, context)
289288
except AttributeInferenceError as error:
290289
raise InferenceError(**vars(error)) from error
291290

292291
def _wrap_attr(
293-
self, attrs: Iterable[InferenceResult], context: InferenceContext | None = None
292+
self, iattrs: Iterable[InferenceResult], context: InferenceContext | None = None
294293
) -> Iterator[InferenceResult]:
295294
"""Wrap bound methods of attrs in a InstanceMethod proxies."""
296-
for attr in attrs:
295+
for attr in iattrs:
297296
if isinstance(attr, UnboundMethod):
298297
if _is_property(attr):
299298
yield from attr.infer_call_result(self, context)

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,14 +2465,11 @@ def igetattr(
24652465
24662466
:returns: The inferred possible values.
24672467
"""
2468-
from astroid import objects # pylint: disable=import-outside-toplevel
2469-
24702468
# set lookup name since this is necessary to infer on import nodes for
24712469
# instance
24722470
context = copy_context(context)
24732471
context.lookupname = name
24742472

2475-
metaclass = self.metaclass(context=context)
24762473
try:
24772474
attributes = self.getattr(name, context, class_context=class_context)
24782475
# If we have more than one attribute, make sure that those starting from
@@ -2495,44 +2492,7 @@ def igetattr(
24952492
for a in attributes
24962493
if a not in functions or a is last_function or bases._is_property(a)
24972494
]
2498-
2499-
for inferred in bases._infer_stmts(attributes, context, frame=self):
2500-
# yield Uninferable object instead of descriptors when necessary
2501-
if not isinstance(inferred, node_classes.Const) and isinstance(
2502-
inferred, bases.Instance
2503-
):
2504-
try:
2505-
inferred._proxied.getattr("__get__", context)
2506-
except AttributeInferenceError:
2507-
yield inferred
2508-
else:
2509-
yield util.Uninferable
2510-
elif isinstance(inferred, objects.Property):
2511-
function = inferred.function
2512-
if not class_context:
2513-
if not context.callcontext:
2514-
context.callcontext = CallContext(
2515-
args=function.args.arguments, callee=function
2516-
)
2517-
# Through an instance so we can solve the property
2518-
yield from function.infer_call_result(
2519-
caller=self, context=context
2520-
)
2521-
# If we're in a class context, we need to determine if the property
2522-
# was defined in the metaclass (a derived class must be a subclass of
2523-
# the metaclass of all its bases), in which case we can resolve the
2524-
# property. If not, i.e. the property is defined in some base class
2525-
# instead, then we return the property object
2526-
elif metaclass and function.parent.scope() is metaclass:
2527-
# Resolve a property as long as it is not accessed through
2528-
# the class itself.
2529-
yield from function.infer_call_result(
2530-
caller=self, context=context
2531-
)
2532-
else:
2533-
yield inferred
2534-
else:
2535-
yield function_to_method(inferred, self)
2495+
yield from self._infer_attrs(attributes, context, class_context)
25362496
except AttributeInferenceError as error:
25372497
if not name.startswith("__") and self.has_dynamic_getattr(context):
25382498
# class handle some dynamic attributes, return a Uninferable object
@@ -2542,6 +2502,49 @@ def igetattr(
25422502
str(error), target=self, attribute=name, context=context
25432503
) from error
25442504

2505+
def _infer_attrs(
2506+
self,
2507+
attributes: list[InferenceResult],
2508+
context: InferenceContext,
2509+
class_context: bool = True,
2510+
) -> Iterator[InferenceResult]:
2511+
from astroid import objects # pylint: disable=import-outside-toplevel
2512+
2513+
metaclass = self.metaclass(context=context)
2514+
for inferred in bases._infer_stmts(attributes, context, frame=self):
2515+
# yield Uninferable object instead of descriptors when necessary
2516+
if not isinstance(inferred, node_classes.Const) and isinstance(
2517+
inferred, bases.Instance
2518+
):
2519+
try:
2520+
inferred._proxied.getattr("__get__", context)
2521+
except AttributeInferenceError:
2522+
yield inferred
2523+
else:
2524+
yield util.Uninferable
2525+
elif isinstance(inferred, objects.Property):
2526+
function = inferred.function
2527+
if not class_context:
2528+
if not context.callcontext:
2529+
context.callcontext = CallContext(
2530+
args=function.args.arguments, callee=function
2531+
)
2532+
# Through an instance so we can solve the property
2533+
yield from function.infer_call_result(caller=self, context=context)
2534+
# If we're in a class context, we need to determine if the property
2535+
# was defined in the metaclass (a derived class must be a subclass of
2536+
# the metaclass of all its bases), in which case we can resolve the
2537+
# property. If not, i.e. the property is defined in some base class
2538+
# instead, then we return the property object
2539+
elif metaclass and function.parent.scope() is metaclass:
2540+
# Resolve a property as long as it is not accessed through
2541+
# the class itself.
2542+
yield from function.infer_call_result(caller=self, context=context)
2543+
else:
2544+
yield inferred
2545+
else:
2546+
yield function_to_method(inferred, self)
2547+
25452548
def has_dynamic_getattr(self, context: InferenceContext | None = None) -> bool:
25462549
"""Check if the class has a custom __getattr__ or __getattribute__.
25472550

tests/test_scoped_nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1295,7 +1295,7 @@ def func(arg1, arg2):
12951295
self.assertIsInstance(inferred[0], BoundMethod)
12961296
inferred = list(Instance(cls).igetattr("m4"))
12971297
self.assertEqual(len(inferred), 1)
1298-
self.assertIsInstance(inferred[0], nodes.FunctionDef)
1298+
self.assertIsInstance(inferred[0], BoundMethod)
12991299

13001300
def test_getattr_from_grandpa(self) -> None:
13011301
data = """

0 commit comments

Comments
 (0)