Skip to content

Commit 514c832

Browse files
authored
Fix recursion error for inference of self-referencing class attribute (#1392)
1 parent 8f477fd commit 514c832

File tree

3 files changed

+60
-3
lines changed

3 files changed

+60
-3
lines changed

ChangeLog

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ Release date: TBA
4343
Closes #1282
4444
Ref #1103
4545

46+
* Fixed crash with recursion error for inference of class attributes that referenced
47+
the class itself.
48+
49+
Closes PyCQA/pylint#5408
50+
4651
* Fixed crash when trying to infer ``items()`` on the ``__dict__``
4752
attribute of an imported module.
4853

astroid/inference_tip.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77

88
import wrapt
99

10-
from astroid.exceptions import InferenceOverwriteError
10+
from astroid import bases, util
11+
from astroid.exceptions import InferenceOverwriteError, UseInferenceDefault
1112
from astroid.nodes import NodeNG
1213

1314
InferFn = typing.Callable[..., typing.Any]
15+
InferOptions = typing.Union[
16+
NodeNG, bases.Instance, bases.UnboundMethod, typing.Type[util.Uninferable]
17+
]
1418

15-
_cache: typing.Dict[typing.Tuple[InferFn, NodeNG], typing.Any] = {}
19+
_cache: typing.Dict[
20+
typing.Tuple[InferFn, NodeNG], typing.Optional[typing.List[InferOptions]]
21+
] = {}
1622

1723

1824
def clear_inference_tip_cache():
@@ -21,13 +27,21 @@ def clear_inference_tip_cache():
2127

2228

2329
@wrapt.decorator
24-
def _inference_tip_cached(func, instance, args, kwargs):
30+
def _inference_tip_cached(
31+
func: InferFn, instance: None, args: typing.Any, kwargs: typing.Any
32+
) -> typing.Iterator[InferOptions]:
2533
"""Cache decorator used for inference tips"""
2634
node = args[0]
2735
try:
2836
result = _cache[func, node]
37+
# If through recursion we end up trying to infer the same
38+
# func + node we raise here.
39+
if result is None:
40+
raise UseInferenceDefault()
2941
except KeyError:
42+
_cache[func, node] = None
3043
result = _cache[func, node] = list(func(*args, **kwargs))
44+
assert result
3145
return iter(result)
3246

3347

tests/unittest_inference.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6621,5 +6621,43 @@ def test_inference_of_items_on_module_dict() -> None:
66216621
builder.file_build(str(DATA_DIR / "module_dict_items_call" / "test.py"), "models")
66226622

66236623

6624+
def test_recursion_on_inference_tip() -> None:
6625+
"""Regression test for recursion in inference tip.
6626+
6627+
Originally reported in https://github.com/PyCQA/pylint/issues/5408.
6628+
"""
6629+
code = """
6630+
class MyInnerClass:
6631+
...
6632+
6633+
6634+
class MySubClass:
6635+
inner_class = MyInnerClass
6636+
6637+
6638+
class MyClass:
6639+
sub_class = MySubClass()
6640+
6641+
6642+
def get_unpatched_class(cls):
6643+
return cls
6644+
6645+
6646+
def get_unpatched(item):
6647+
lookup = get_unpatched_class if isinstance(item, type) else lambda item: None
6648+
return lookup(item)
6649+
6650+
6651+
_Child = get_unpatched(MyClass.sub_class.inner_class)
6652+
6653+
6654+
class Child(_Child):
6655+
def patch(cls):
6656+
MyClass.sub_class.inner_class = cls
6657+
"""
6658+
module = parse(code)
6659+
assert module
6660+
6661+
66246662
if __name__ == "__main__":
66256663
unittest.main()

0 commit comments

Comments
 (0)