Skip to content

Commit ce64792

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 b4ac0e2 commit ce64792

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
@@ -2458,14 +2458,11 @@ def igetattr(
24582458
24592459
:returns: The inferred possible values.
24602460
"""
2461-
from astroid import objects # pylint: disable=import-outside-toplevel
2462-
24632461
# set lookup name since this is necessary to infer on import nodes for
24642462
# instance
24652463
context = copy_context(context)
24662464
context.lookupname = name
24672465

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

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

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)