Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix interrupted InferenceContext call chains #2209

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion astroid/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
19 changes: 15 additions & 4 deletions astroid/interpreter/dunder_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, [])
Expand All @@ -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
Expand All @@ -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)

Expand Down
12 changes: 6 additions & 6 deletions astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion tests/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down