Skip to content
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 @@ -37,6 +37,11 @@ Release date: TBA
Closes #1282
Ref #1103

* Fixed crash with recursion error for inference of class attributes that referenced
the class itself.

Closes PyCQA/pylint#5408

* Fixed crash when trying to infer ``items()`` on the ``__dict__``
attribute of an imported module.

Expand Down
20 changes: 17 additions & 3 deletions astroid/inference_tip.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@

import wrapt

from astroid.exceptions import InferenceOverwriteError
from astroid import bases, util
from astroid.exceptions import InferenceOverwriteError, UseInferenceDefault
from astroid.nodes import NodeNG

InferFn = typing.Callable[..., typing.Any]
InferOptions = typing.Union[
NodeNG, bases.Instance, bases.UnboundMethod, typing.Type[util.Uninferable]
]

_cache: typing.Dict[typing.Tuple[InferFn, NodeNG], typing.Any] = {}
_cache: typing.Dict[
typing.Tuple[InferFn, NodeNG], typing.Optional[typing.List[InferOptions]]
] = {}


def clear_inference_tip_cache():
Expand All @@ -21,13 +27,21 @@ def clear_inference_tip_cache():


@wrapt.decorator
def _inference_tip_cached(func, instance, args, kwargs):
def _inference_tip_cached(
func: InferFn, instance: None, args: typing.Any, kwargs: typing.Any
) -> typing.Iterator[InferOptions]:
"""Cache decorator used for inference tips"""
node = args[0]
try:
result = _cache[func, node]
# If through recursion we end up trying to infer the same
# func + node we raise here.
if result is None:
raise UseInferenceDefault()
except KeyError:
_cache[func, node] = None
result = _cache[func, node] = list(func(*args, **kwargs))
assert result
return iter(result)


Expand Down
38 changes: 38 additions & 0 deletions tests/unittest_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -6621,5 +6621,43 @@ def test_inference_of_items_on_module_dict() -> None:
builder.file_build(str(DATA_DIR / "module_dict_items_call" / "test.py"), "models")


def test_recursion_on_inference_tip() -> None:
"""Regression test for recursion in inference tip.

Originally reported in https://github.com/PyCQA/pylint/issues/5408.
"""
code = """
class MyInnerClass:
...


class MySubClass:
inner_class = MyInnerClass


class MyClass:
sub_class = MySubClass()


def get_unpatched_class(cls):
return cls


def get_unpatched(item):
lookup = get_unpatched_class if isinstance(item, type) else lambda item: None
return lookup(item)


_Child = get_unpatched(MyClass.sub_class.inner_class)


class Child(_Child):
def patch(cls):
MyClass.sub_class.inner_class = cls
"""
module = parse(code)
assert module


if __name__ == "__main__":
unittest.main()