Skip to content

Commit 0740a0d

Browse files
Complete cache key for inference tip (#2158)
The cache key was lacking the `context` arg. Co-authored-by: Sylvain Ackermann <[email protected]>
1 parent 06fafc4 commit 0740a0d

File tree

5 files changed

+53
-19
lines changed

5 files changed

+53
-19
lines changed

ChangeLog

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ Release date: TBA
1717

1818
* Reduce file system access in ``ast_from_file()``.
1919

20+
* Fix incorrect cache keys for inference results, thereby correctly inferring types
21+
for calls instantiating types dynamically.
22+
23+
Closes #1828
24+
Closes pylint-dev/pylint#7464
25+
Closes pylint-dev/pylint#8074
26+
2027
* ``nodes.FunctionDef`` no longer inherits from ``nodes.Lambda``.
2128
This is a breaking change but considered a bug fix as the nodes did not share the same
2229
API and were not interchangeable.

astroid/inference_tip.py

+22-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
from collections.abc import Callable, Iterator
1111

12+
from astroid.context import InferenceContext
1213
from astroid.exceptions import InferenceOverwriteError, UseInferenceDefault
1314
from astroid.nodes import NodeNG
1415
from astroid.typing import InferenceResult, InferFn
@@ -20,7 +21,11 @@
2021

2122
_P = ParamSpec("_P")
2223

23-
_cache: dict[tuple[InferFn, NodeNG], list[InferenceResult] | None] = {}
24+
_cache: dict[
25+
tuple[InferFn, NodeNG, InferenceContext | None], list[InferenceResult]
26+
] = {}
27+
28+
_CURRENTLY_INFERRING: set[tuple[InferFn, NodeNG]] = set()
2429

2530

2631
def clear_inference_tip_cache() -> None:
@@ -35,16 +40,25 @@ def _inference_tip_cached(
3540

3641
def inner(*args: _P.args, **kwargs: _P.kwargs) -> Iterator[InferenceResult]:
3742
node = args[0]
38-
try:
39-
result = _cache[func, node]
43+
context = args[1]
44+
partial_cache_key = (func, node)
45+
if partial_cache_key in _CURRENTLY_INFERRING:
4046
# If through recursion we end up trying to infer the same
4147
# func + node we raise here.
42-
if result is None:
43-
raise UseInferenceDefault()
48+
raise UseInferenceDefault
49+
try:
50+
return _cache[func, node, context]
4451
except KeyError:
45-
_cache[func, node] = None
46-
result = _cache[func, node] = list(func(*args, **kwargs))
47-
assert result
52+
# Recursion guard with a partial cache key.
53+
# Using the full key causes a recursion error on PyPy.
54+
# It's a pragmatic compromise to avoid so much recursive inference
55+
# with slightly different contexts while still passing the simple
56+
# test cases included with this commit.
57+
_CURRENTLY_INFERRING.add(partial_cache_key)
58+
result = _cache[func, node, context] = list(func(*args, **kwargs))
59+
# Remove recursion guard.
60+
_CURRENTLY_INFERRING.remove(partial_cache_key)
61+
4862
return iter(result)
4963

5064
return inner

tests/brain/test_brain.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -930,13 +930,7 @@ class A:
930930
assert inferred.value == 42
931931

932932
def test_typing_cast_multiple_inference_calls(self) -> None:
933-
"""Inference of an outer function should not store the result for cast.
934-
935-
https://github.com/pylint-dev/pylint/issues/8074
936-
937-
Possible solution caused RecursionErrors with Python 3.8 and CPython + PyPy.
938-
https://github.com/pylint-dev/astroid/pull/1982
939-
"""
933+
"""Inference of an outer function should not store the result for cast."""
940934
ast_nodes = builder.extract_node(
941935
"""
942936
from typing import TypeVar, cast
@@ -954,7 +948,7 @@ def ident(var: T) -> T:
954948

955949
i1 = next(ast_nodes[1].infer())
956950
assert isinstance(i1, nodes.Const)
957-
assert i1.value == 2 # should be "Hello"!
951+
assert i1.value == "Hello"
958952

959953

960954
class ReBrainTest(unittest.TestCase):

tests/test_regrtest.py

+21
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,27 @@ def d(self):
336336
assert isinstance(inferred, Instance)
337337
assert inferred.qname() == ".A"
338338

339+
def test_inference_context_consideration(self) -> None:
340+
"""https://github.com/PyCQA/astroid/issues/1828"""
341+
code = """
342+
class Base:
343+
def return_type(self):
344+
return type(self)()
345+
class A(Base):
346+
def method(self):
347+
return self.return_type()
348+
class B(Base):
349+
def method(self):
350+
return self.return_type()
351+
A().method() #@
352+
B().method() #@
353+
"""
354+
node1, node2 = extract_node(code)
355+
inferred1 = next(node1.infer())
356+
assert inferred1.qname() == ".A"
357+
inferred2 = next(node2.infer())
358+
assert inferred2.qname() == ".B"
359+
339360

340361
class Whatever:
341362
a = property(lambda x: x, lambda x: x) # type: ignore[misc]

tests/test_scoped_nodes.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -1771,9 +1771,7 @@ def __init__(self):
17711771
"FinalClass",
17721772
"ClassB",
17731773
"MixinB",
1774-
# We don't recognize what 'cls' is at time of .format() call, only
1775-
# what it is at the end.
1776-
# "strMixin",
1774+
"strMixin",
17771775
"ClassA",
17781776
"MixinA",
17791777
"intMixin",

0 commit comments

Comments
 (0)