Skip to content
Closed
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 docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Version 2.5.2

**Bugs Fixed**

* Fixed ``wrapt.lru_cache`` retaining the class from the first call to a
cached class method and using it for calls through subclasses. The cache
remains shared, but now includes the bound class in its key, as
``functools.lru_cache`` does.

* The special methods added to the class generated for an
``wrapt.AutoObjectProxy``, and adjusted when ``__wrapped__`` was
reassigned, were determined by inspecting the wrapped object itself,
Expand Down
17 changes: 14 additions & 3 deletions src/wrapt/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,27 @@ def __call__(self, *args, **kwargs):

# For class methods and static methods, use a single shared
# cache stored on the parent wrapper. The cache is created
# from the bound __wrapped__ since the parent's __wrapped__
# is the raw classmethod/staticmethod descriptor.
# from __wrapped__ since the parent's __wrapped__ is the raw
# classmethod/staticmethod descriptor. For class methods, cache
# the unbound function so the bound class can be a cache key.

if not self._is_instance_method():
if parent._self_cache is None:
with synchronized(parent):
if parent._self_cache is None:
wrapped = self.__wrapped__
if self._self_binding == "classmethod":
wrapped = wrapped.__func__
parent._self_cache = _functools_lru_cache(
**parent._self_lru_kwargs
)(self.__wrapped__)
)(wrapped)

# Keep the cache shared, but include the bound class in its key.
# Caching the bound method itself would retain the class of the
# first call and incorrectly use it for inherited calls too.

if self._self_binding == "classmethod":
return parent._self_cache(self.__wrapped__.__self__, *args, **kwargs)

return parent._self_cache(*args, **kwargs)

Expand Down
31 changes: 31 additions & 0 deletions tests/core/test_lru_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,37 @@ def test_maxsize_passed_through(self):
self.assertEqual(info.maxsize, 32)


class TestInheritedClassMethod(unittest.TestCase):
def test_preserves_bound_class_and_shared_cache(self):
for child_first in (False, True):
with self.subTest(child_first=child_first):
calls = []

class Base:
@wrapt.lru_cache(maxsize=32)
@classmethod
def method(cls, value):
calls.append((cls, value))
return cls, value

class Child(Base):
pass

first, second = (Child, Base) if child_first else (Base, Child)
self.assertEqual(first.method(1), (first, 1))
self.assertEqual(second.method(1), (second, 1))
self.assertEqual(second.method(2), (second, 2))
self.assertEqual(first().method(1), (first, 1))
self.assertEqual(second().method(1), (second, 1))
self.assertEqual(calls, [(first, 1), (second, 1), (second, 2)])
info = Base.method.cache_info()
self.assertEqual(info, Child.method.cache_info())
self.assertEqual((info.hits, info.misses, info.currsize), (2, 3, 3))
self.assertEqual(info.maxsize, 32)
Child.method.cache_clear()
self.assertEqual(Base.method.cache_info().currsize, 0)


class TestStaticMethod(unittest.TestCase):
def setUp(self):
CachedClass.static_method.cache_clear()
Expand Down
Loading