Skip to content

Commit 1425819

Browse files
committed
Refine the implementation of cached_method
Thanks to feedback from @jaraco, this adjustment makes the public interface for `cached_method` a function which is responsible for the argument management, rather than trying to use `__call__` on the descriptor itself. This frees up `__call__` to lookup the relevant LRU-cached function and invoke it, which makes the `_cached_method` descriptor suitable for use as a `property.fget` callable. Tests are updated to indicate that a decorator can be prepared and then used repeatedly (which was previously an explicit error), and can be used under a property decorator.
1 parent a5b4b0d commit 1425819

2 files changed

Lines changed: 45 additions & 30 deletions

File tree

Lib/functools.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,7 +1202,7 @@ def wrapped(*args, **kwargs):
12021202
return wrapped
12031203

12041204

1205-
class cached_method:
1205+
class _cached_method:
12061206
"""
12071207
A caching decorator for use on instance methods.
12081208
@@ -1215,35 +1215,31 @@ class cached_method:
12151215
By default, this provides an infinite sized cache similar to functools.cache. Use
12161216
*maxsize* and *typed* to set these attributes of the underlying LRU cache.
12171217
"""
1218-
def __init__(self, func=None, /, maxsize=None, typed=False):
1219-
self.func = None
1220-
self._maxsize = maxsize
1221-
self._typed = typed
1218+
def __init__(self, func, /, maxsize=None, typed=False):
12221219
self._function_table = {}
12231220
# we need a lock when initializing per-instance caches
12241221
self._cache_init_lock = RLock()
12251222

1226-
if func is not None:
1227-
self.func = func
1228-
update_wrapper(self, func)
1223+
self._maxsize = maxsize
1224+
self._typed = typed
12291225

1230-
def __call__(self, func):
1231-
if self.func is not None:
1232-
raise TypeError(
1233-
"Each cached_method decorator can only apply to one function."
1234-
)
12351226
self.func = func
12361227
update_wrapper(self, func)
1237-
return self
1228+
1229+
def __call__(self, instance, *args, **kwargs):
1230+
cached_func = self._get_or_create_cached_func(instance)
1231+
return cached_func(*args, **kwargs)
12381232

12391233
def __get__(self, instance, owner=None):
1234+
if instance is None:
1235+
return self
1236+
return self._get_or_create_cached_func(instance)
1237+
1238+
def _get_or_create_cached_func(self, instance):
12401239
# similar to singledispatch(), we want to defer use of weakref until/unless it
12411240
# is needed
12421241
import weakref
12431242

1244-
if instance is None:
1245-
return self
1246-
12471243
instance_id = id(instance)
12481244

12491245
# first try to retrieve the cached func without locking (thus avoiding any
@@ -1269,3 +1265,12 @@ def __get__(self, instance, owner=None):
12691265
self._function_table[instance_id] = ref, cached_func
12701266

12711267
return cached_func
1268+
1269+
1270+
def cached_method(func=None, /, maxsize=None, typed=False):
1271+
if func is None:
1272+
def decorator(func):
1273+
return _cached_method(func, maxsize=maxsize, typed=typed)
1274+
return decorator
1275+
else:
1276+
return _cached_method(func, maxsize=maxsize, typed=typed)

Lib/test/test_functools.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3843,21 +3843,31 @@ def test_cache_info(self):
38433843
self.assertEqual(one.add.cache_info(),
38443844
self.module._CacheInfo(hits=0, misses=0, maxsize=None, currsize=0))
38453845

3846-
def test_reapplication_causes_type_error(self):
3847-
with self.assertRaisesRegex(
3848-
TypeError,
3849-
r"Each cached_method decorator can only apply to one function\.",
3850-
):
3851-
decorator = py_functools.cached_method()
3846+
def test_reapplication_is_allowed(self):
3847+
decorator = py_functools.cached_method(maxsize=10)
38523848

3853-
class MyObject:
3854-
@decorator
3855-
def a(self):
3856-
return None
3849+
class MyObject:
3850+
@decorator
3851+
def a(self):
3852+
return 1
3853+
3854+
@decorator
3855+
def b(self):
3856+
return 2
3857+
3858+
x = MyObject()
3859+
self.assertEqual(x.a(), 1)
3860+
self.assertEqual(x.b(), 2)
3861+
3862+
def test_cached_method_under_property(self):
3863+
class MyObject:
3864+
@property
3865+
@py_functools.cached_method
3866+
def foo(self):
3867+
return 1
38573868

3858-
@decorator
3859-
def b(self):
3860-
return None
3869+
x = MyObject()
3870+
self.assertEqual(x.foo, 1)
38613871

38623872

38633873
if __name__ == '__main__':

0 commit comments

Comments
 (0)