bpo-35780: Fix errors in lru_cache() C code (GH-11623)
diff --git a/Lib/functools.py b/Lib/functools.py
index ab7d71e..6233c30 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -454,7 +454,7 @@
def _make_key(args, kwds, typed,
kwd_mark = (object(),),
- fasttypes = {int, str, frozenset, type(None)},
+ fasttypes = {int, str},
tuple=tuple, type=type, len=len):
"""Make a cache key from optionally typed positional and keyword arguments
@@ -510,8 +510,11 @@
# Early detection of an erroneous call to @lru_cache without any arguments
# resulting in the inner function being passed to maxsize instead of an
- # integer or None.
- if maxsize is not None and not isinstance(maxsize, int):
+ # integer or None. Negative maxsize is treated as 0.
+ if isinstance(maxsize, int):
+ if maxsize < 0:
+ maxsize = 0
+ elif maxsize is not None:
raise TypeError('Expected maxsize to be an integer or None')
def decorating_function(user_function):
@@ -578,6 +581,7 @@
link[NEXT] = root
hits += 1
return result
+ misses += 1
result = user_function(*args, **kwds)
with lock:
if key in cache:
@@ -615,7 +619,6 @@
# Use the cache_len bound method instead of the len() function
# which could potentially be wrapped in an lru_cache itself.
full = (cache_len() >= maxsize)
- misses += 1
return result
def cache_info():
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index ffbd0fc..63a9ade 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -1233,6 +1233,33 @@
self.assertEqual(misses, 4)
self.assertEqual(currsize, 2)
+ def test_lru_bug_35780(self):
+ # C version of the lru_cache was not checking to see if
+ # the user function call has already modified the cache
+ # (this arises in recursive calls and in multi-threading).
+ # This cause the cache to have orphan links not referenced
+ # by the cache dictionary.
+
+ once = True # Modified by f(x) below
+
+ @self.module.lru_cache(maxsize=10)
+ def f(x):
+ nonlocal once
+ rv = f'.{x}.'
+ if x == 20 and once:
+ once = False
+ rv = f(x)
+ return rv
+
+ # Fill the cache
+ for x in range(15):
+ self.assertEqual(f(x), f'.{x}.')
+ self.assertEqual(f.cache_info().currsize, 10)
+
+ # Make a recursive call and make sure the cache remains full
+ self.assertEqual(f(20), '.20.')
+ self.assertEqual(f.cache_info().currsize, 10)
+
def test_lru_hash_only_once(self):
# To protect against weird reentrancy bugs and to improve
# efficiency when faced with slow __hash__ methods, the
@@ -1329,7 +1356,7 @@
for i in (0, 1):
self.assertEqual([eq(n) for n in range(150)], list(range(150)))
self.assertEqual(eq.cache_info(),
- self.module._CacheInfo(hits=0, misses=300, maxsize=-10, currsize=1))
+ self.module._CacheInfo(hits=0, misses=300, maxsize=0, currsize=0))
def test_lru_with_exceptions(self):
# Verify that user_function exceptions get passed through without