diff --git a/ChangeLog b/ChangeLog index 95fb9066d..ab0b1ef33 100644 --- a/ChangeLog +++ b/ChangeLog @@ -30,6 +30,11 @@ Release date: TBA Closes pylint-dev/pylint#7464 Closes pylint-dev/pylint#8074 +* Fix interrupted ``InferenceContext`` call chains, thereby addressing performance + problems when linting ``sqlalchemy``. + + Closes pylint-dev/pylint#8150 + * ``nodes.FunctionDef`` no longer inherits from ``nodes.Lambda``. This is a breaking change but considered a bug fix as the nodes did not share the same API and were not interchangeable. diff --git a/astroid/arguments.py b/astroid/arguments.py index c92a5ffea..f24612bd8 100644 --- a/astroid/arguments.py +++ b/astroid/arguments.py @@ -215,7 +215,7 @@ def infer_argument( # `cls.metaclass_method`. In this case, the # first argument is always the class. method_scope = funcnode.parent.scope() - if method_scope is boundnode.metaclass(): + if method_scope is boundnode.metaclass(context=context): return iter((boundnode,)) if funcnode.type == "method": diff --git a/astroid/interpreter/dunder_lookup.py b/astroid/interpreter/dunder_lookup.py index 0f169043d..727c1ad46 100644 --- a/astroid/interpreter/dunder_lookup.py +++ b/astroid/interpreter/dunder_lookup.py @@ -12,11 +12,18 @@ As such, the lookup for the special methods is actually simpler than the dot attribute access. """ +from __future__ import annotations + import itertools +from typing import TYPE_CHECKING import astroid from astroid.exceptions import AttributeInferenceError +if TYPE_CHECKING: + from astroid import nodes + from astroid.context import InferenceContext + def _lookup_in_mro(node, name) -> list: attrs = node.locals.get(name, []) @@ -31,7 +38,9 @@ def _lookup_in_mro(node, name) -> list: return values -def lookup(node, name) -> list: +def lookup( + node: nodes.NodeNG, name: str, context: InferenceContext | None = None +) -> list: """Lookup the given special method name in the given *node*. If the special method was found, then a list of attributes @@ -45,13 +54,15 @@ def lookup(node, name) -> list: if isinstance(node, astroid.Instance): return _lookup_in_mro(node, name) if isinstance(node, astroid.ClassDef): - return _class_lookup(node, name) + return _class_lookup(node, name, context=context) raise AttributeInferenceError(attribute=name, target=node) -def _class_lookup(node, name) -> list: - metaclass = node.metaclass() +def _class_lookup( + node: nodes.ClassDef, name: str, context: InferenceContext | None = None +) -> list: + metaclass = node.metaclass(context=context) if metaclass is None: raise AttributeInferenceError(attribute=name, target=node) diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py index 20f828e32..333933817 100644 --- a/astroid/nodes/scoped_nodes/scoped_nodes.py +++ b/astroid/nodes/scoped_nodes/scoped_nodes.py @@ -1666,7 +1666,7 @@ def _rec_get_names(args, names: list[str] | None = None) -> list[str]: return names -def _is_metaclass(klass, seen=None) -> bool: +def _is_metaclass(klass, seen=None, context: InferenceContext | None = None) -> bool: """Return if the given class can be used as a metaclass. """ @@ -1676,7 +1676,7 @@ def _is_metaclass(klass, seen=None) -> bool: seen = set() for base in klass.bases: try: - for baseobj in base.infer(): + for baseobj in base.infer(context=context): baseobj_name = baseobj.qname() if baseobj_name in seen: continue @@ -1691,21 +1691,21 @@ def _is_metaclass(klass, seen=None) -> bool: continue if baseobj._type == "metaclass": return True - if _is_metaclass(baseobj, seen): + if _is_metaclass(baseobj, seen, context=context): return True except InferenceError: continue return False -def _class_type(klass, ancestors=None): +def _class_type(klass, ancestors=None, context: InferenceContext | None = None): """return a ClassDef node type to differ metaclass and exception from 'regular' classes """ # XXX we have to store ancestors in case we have an ancestor loop if klass._type is not None: return klass._type - if _is_metaclass(klass): + if _is_metaclass(klass, context=context): klass._type = "metaclass" elif klass.name.endswith("Exception"): klass._type = "exception" @@ -2502,7 +2502,7 @@ def getitem(self, index, context: InferenceContext | None = None): ``__getitem__`` method. """ try: - methods = lookup(self, "__getitem__") + methods = lookup(self, "__getitem__", context=context) except AttributeInferenceError as exc: if isinstance(self, ClassDef): # subscripting a class definition may be diff --git a/tests/test_inference.py b/tests/test_inference.py index 49e85c416..6760f9c91 100644 --- a/tests/test_inference.py +++ b/tests/test_inference.py @@ -4235,7 +4235,7 @@ class Clazz(metaclass=_Meta): Clazz() #@ """ ).inferred()[0] - assert isinstance(cls, nodes.ClassDef) and cls.name == "Clazz" + assert isinstance(cls, Instance) and cls.name == "Clazz" def test_infer_subclass_attr_outer_class(self) -> None: node = extract_node( @@ -4908,6 +4908,21 @@ def __class_getitem__(cls, *args, **kwargs): self.assertIsInstance(inferred, nodes.ClassDef) self.assertEqual(inferred.name, "Foo") + def test_class_subscript_inference_context(self) -> None: + """Context path has a reference to any parents inferred by getitem().""" + code = """ + class Parent: pass + + class A(Parent): + def __class_getitem__(self, value): + return cls + """ + klass = extract_node(code) + context = InferenceContext() + _ = klass.getitem(0, context=context) + + assert list(context.path)[0][0].name == "Parent" + class TestType(unittest.TestCase): def test_type(self) -> None: