Skip to content

Commit 61ca2e8

Browse files
Fix interrupted InferenceContext call chains (#2209)
ClassDef.getitem() and infer_argument() both had interrupted call chains where InferenceContext wasn't passed all the way through to infer(). This caused performance problems in packages such as sqlalchemy needing these features.
1 parent 1fbbf25 commit 61ca2e8

File tree

5 files changed

+43
-12
lines changed

5 files changed

+43
-12
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ Release date: TBA
3030
Closes pylint-dev/pylint#7464
3131
Closes pylint-dev/pylint#8074
3232

33+
* Fix interrupted ``InferenceContext`` call chains, thereby addressing performance
34+
problems when linting ``sqlalchemy``.
35+
36+
Closes pylint-dev/pylint#8150
37+
3338
* ``nodes.FunctionDef`` no longer inherits from ``nodes.Lambda``.
3439
This is a breaking change but considered a bug fix as the nodes did not share the same
3540
API and were not interchangeable.

astroid/arguments.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def infer_argument(
215215
# `cls.metaclass_method`. In this case, the
216216
# first argument is always the class.
217217
method_scope = funcnode.parent.scope()
218-
if method_scope is boundnode.metaclass():
218+
if method_scope is boundnode.metaclass(context=context):
219219
return iter((boundnode,))
220220

221221
if funcnode.type == "method":

astroid/interpreter/dunder_lookup.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@
1212
As such, the lookup for the special methods is actually simpler than
1313
the dot attribute access.
1414
"""
15+
from __future__ import annotations
16+
1517
import itertools
18+
from typing import TYPE_CHECKING
1619

1720
import astroid
1821
from astroid.exceptions import AttributeInferenceError
1922

23+
if TYPE_CHECKING:
24+
from astroid import nodes
25+
from astroid.context import InferenceContext
26+
2027

2128
def _lookup_in_mro(node, name) -> list:
2229
attrs = node.locals.get(name, [])
@@ -31,7 +38,9 @@ def _lookup_in_mro(node, name) -> list:
3138
return values
3239

3340

34-
def lookup(node, name) -> list:
41+
def lookup(
42+
node: nodes.NodeNG, name: str, context: InferenceContext | None = None
43+
) -> list:
3544
"""Lookup the given special method name in the given *node*.
3645
3746
If the special method was found, then a list of attributes
@@ -45,13 +54,15 @@ def lookup(node, name) -> list:
4554
if isinstance(node, astroid.Instance):
4655
return _lookup_in_mro(node, name)
4756
if isinstance(node, astroid.ClassDef):
48-
return _class_lookup(node, name)
57+
return _class_lookup(node, name, context=context)
4958

5059
raise AttributeInferenceError(attribute=name, target=node)
5160

5261

53-
def _class_lookup(node, name) -> list:
54-
metaclass = node.metaclass()
62+
def _class_lookup(
63+
node: nodes.ClassDef, name: str, context: InferenceContext | None = None
64+
) -> list:
65+
metaclass = node.metaclass(context=context)
5566
if metaclass is None:
5667
raise AttributeInferenceError(attribute=name, target=node)
5768

astroid/nodes/scoped_nodes/scoped_nodes.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ def _rec_get_names(args, names: list[str] | None = None) -> list[str]:
16661666
return names
16671667

16681668

1669-
def _is_metaclass(klass, seen=None) -> bool:
1669+
def _is_metaclass(klass, seen=None, context: InferenceContext | None = None) -> bool:
16701670
"""Return if the given class can be
16711671
used as a metaclass.
16721672
"""
@@ -1676,7 +1676,7 @@ def _is_metaclass(klass, seen=None) -> bool:
16761676
seen = set()
16771677
for base in klass.bases:
16781678
try:
1679-
for baseobj in base.infer():
1679+
for baseobj in base.infer(context=context):
16801680
baseobj_name = baseobj.qname()
16811681
if baseobj_name in seen:
16821682
continue
@@ -1691,21 +1691,21 @@ def _is_metaclass(klass, seen=None) -> bool:
16911691
continue
16921692
if baseobj._type == "metaclass":
16931693
return True
1694-
if _is_metaclass(baseobj, seen):
1694+
if _is_metaclass(baseobj, seen, context=context):
16951695
return True
16961696
except InferenceError:
16971697
continue
16981698
return False
16991699

17001700

1701-
def _class_type(klass, ancestors=None):
1701+
def _class_type(klass, ancestors=None, context: InferenceContext | None = None):
17021702
"""return a ClassDef node type to differ metaclass and exception
17031703
from 'regular' classes
17041704
"""
17051705
# XXX we have to store ancestors in case we have an ancestor loop
17061706
if klass._type is not None:
17071707
return klass._type
1708-
if _is_metaclass(klass):
1708+
if _is_metaclass(klass, context=context):
17091709
klass._type = "metaclass"
17101710
elif klass.name.endswith("Exception"):
17111711
klass._type = "exception"
@@ -2502,7 +2502,7 @@ def getitem(self, index, context: InferenceContext | None = None):
25022502
``__getitem__`` method.
25032503
"""
25042504
try:
2505-
methods = lookup(self, "__getitem__")
2505+
methods = lookup(self, "__getitem__", context=context)
25062506
except AttributeInferenceError as exc:
25072507
if isinstance(self, ClassDef):
25082508
# subscripting a class definition may be

tests/test_inference.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4235,7 +4235,7 @@ class Clazz(metaclass=_Meta):
42354235
Clazz() #@
42364236
"""
42374237
).inferred()[0]
4238-
assert isinstance(cls, nodes.ClassDef) and cls.name == "Clazz"
4238+
assert isinstance(cls, Instance) and cls.name == "Clazz"
42394239

42404240
def test_infer_subclass_attr_outer_class(self) -> None:
42414241
node = extract_node(
@@ -4908,6 +4908,21 @@ def __class_getitem__(cls, *args, **kwargs):
49084908
self.assertIsInstance(inferred, nodes.ClassDef)
49094909
self.assertEqual(inferred.name, "Foo")
49104910

4911+
def test_class_subscript_inference_context(self) -> None:
4912+
"""Context path has a reference to any parents inferred by getitem()."""
4913+
code = """
4914+
class Parent: pass
4915+
4916+
class A(Parent):
4917+
def __class_getitem__(self, value):
4918+
return cls
4919+
"""
4920+
klass = extract_node(code)
4921+
context = InferenceContext()
4922+
_ = klass.getitem(0, context=context)
4923+
4924+
assert list(context.path)[0][0].name == "Parent"
4925+
49114926

49124927
class TestType(unittest.TestCase):
49134928
def test_type(self) -> None:

0 commit comments

Comments
 (0)