diff --git a/docs/index.rst b/docs/index.rst index d6ea285..8d83094 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -286,26 +286,18 @@ often called with the same arguments: cache object. The underlying wrapped function will be called outside the `with` statement, and must be thread-safe by itself. - The original underlying function is accessible through the - :attr:`__wrapped__` attribute of the memoizing wrapper function. - This can be used for introspection or for bypassing the cache. - - To perform operations on the cache object, for example to clear the - cache during runtime, the cache should be assigned to a variable. - When a `lock` object is used, any access to the cache from outside - the function wrapper should also be performed within an appropriate - `with` statement: + The decorator's `cache`, `key` and `lock` parameters are also + available as :attr:`cache`, :attr:`cache_key` and + :attr:`cache_lock` attributes of the memoizing wrapper function. + These can be used for clearing the cache or invalidating individual + cache items, for example. .. testcode:: - from cachetools.keys import hashkey from threading import Lock # 640K should be enough for anyone... - cache = LRUCache(maxsize=640*1024, getsizeof=len) - lock = Lock() - - @cached(cache, key=hashkey, lock=lock) + @cached(cache=LRUCache(maxsize=640*1024, getsizeof=len), lock=Lock()) def get_pep(num): 'Retrieve text of a Python Enhancement Proposal' url = 'http://www.python.org/dev/peps/pep-%04d/' % num @@ -313,12 +305,16 @@ often called with the same arguments: return s.read() # make sure access to cache is synchronized - with lock: - cache.clear() + with get_pep.cache_lock: + get_pep.cache.clear() # always use the key function for accessing cache items - with lock: - cache.pop(hashkey(42), None) + with get_pep.cache_lock: + get_pep.cache.pop(get_pep.cache_key(42), None) + + The original underlying function is accessible through the + :attr:`__wrapped__` attribute. This can be used for introspection + or for bypassing the cache. It is also possible to use a single shared cache object with multiple functions. However, care must be taken that different @@ -397,7 +393,6 @@ often called with the same arguments: PEP #1: ... - When using a shared cache for multiple methods, be aware that different cache keys must be created for each method even when function arguments are the same, just as with the `@cached` diff --git a/src/cachetools/__init__.py b/src/cachetools/__init__.py index 03b6f25..7d8b85d 100644 --- a/src/cachetools/__init__.py +++ b/src/cachetools/__init__.py @@ -663,6 +663,10 @@ def wrapper(*args, **kwargs): except ValueError: return v # value too large + wrapper.cache = cache + wrapper.cache_key = key + wrapper.cache_lock = lock + return functools.update_wrapper(wrapper, func) return decorator @@ -713,6 +717,10 @@ def wrapper(self, *args, **kwargs): except ValueError: return v # value too large + wrapper.cache = cache + wrapper.cache_key = key + wrapper.cache_lock = lock + return functools.update_wrapper(wrapper, method) return decorator diff --git a/tests/test_wrapper.py b/tests/test_cached.py similarity index 80% rename from tests/test_wrapper.py rename to tests/test_cached.py index 37af16b..046cd2c 100644 --- a/tests/test_wrapper.py +++ b/tests/test_cached.py @@ -1,3 +1,4 @@ +import contextlib import unittest import cachetools @@ -20,8 +21,6 @@ def test_decorator(self): wrapper = cachetools.cached(cache)(self.func) self.assertEqual(len(cache), 0) - self.assertEqual(wrapper.__wrapped__, self.func) - self.assertEqual(wrapper(0), 0) self.assertEqual(len(cache), 1) self.assertIn(cachetools.keys.hashkey(0), cache) @@ -49,8 +48,6 @@ def test_decorator_typed(self): wrapper = cachetools.cached(cache, key=key)(self.func) self.assertEqual(len(cache), 0) - self.assertEqual(wrapper.__wrapped__, self.func) - self.assertEqual(wrapper(0), 0) self.assertEqual(len(cache), 1) self.assertIn(cachetools.keys.typedkey(0), cache) @@ -90,7 +87,6 @@ def __exit__(self, *exc): wrapper = cachetools.cached(cache, lock=Lock())(self.func) self.assertEqual(len(cache), 0) - self.assertEqual(wrapper.__wrapped__, self.func) self.assertEqual(wrapper(0), 0) self.assertEqual(Lock.count, 2) self.assertEqual(wrapper(1), 1) @@ -98,6 +94,37 @@ def __exit__(self, *exc): self.assertEqual(wrapper(1), 1) self.assertEqual(Lock.count, 5) + def test_decorator_wrapped(self): + cache = self.cache(2) + wrapper = cachetools.cached(cache)(self.func) + + self.assertEqual(wrapper.__wrapped__, self.func) + + self.assertEqual(len(cache), 0) + self.assertEqual(wrapper.__wrapped__(0), 0) + self.assertEqual(len(cache), 0) + self.assertEqual(wrapper(0), 1) + self.assertEqual(len(cache), 1) + self.assertEqual(wrapper(0), 1) + self.assertEqual(len(cache), 1) + + def test_decorator_attributes(self): + cache = self.cache(2) + wrapper = cachetools.cached(cache)(self.func) + + self.assertIs(wrapper.cache, cache) + self.assertIs(wrapper.cache_key, cachetools.keys.hashkey) + self.assertIs(wrapper.cache_lock, None) + + def test_decorator_attributes_lock(self): + cache = self.cache(2) + lock = contextlib.nullcontext() + wrapper = cachetools.cached(cache, lock=lock)(self.func) + + self.assertIs(wrapper.cache, cache) + self.assertIs(wrapper.cache_key, cachetools.keys.hashkey) + self.assertIs(wrapper.cache_lock, lock) + class CacheWrapperTest(unittest.TestCase, DecoratorTestMixin): def cache(self, minsize): @@ -108,8 +135,6 @@ def test_zero_size_cache_decorator(self): wrapper = cachetools.cached(cache)(self.func) self.assertEqual(len(cache), 0) - self.assertEqual(wrapper.__wrapped__, self.func) - self.assertEqual(wrapper(0), 0) self.assertEqual(len(cache), 0) @@ -128,8 +153,6 @@ def __exit__(self, *exc): wrapper = cachetools.cached(cache, lock=Lock())(self.func) self.assertEqual(len(cache), 0) - self.assertEqual(wrapper.__wrapped__, self.func) - self.assertEqual(wrapper(0), 0) self.assertEqual(len(cache), 0) self.assertEqual(Lock.count, 2) @@ -146,7 +169,6 @@ def func(self, *args, **kwargs): def test_decorator(self): wrapper = cachetools.cached(None)(self.func) - self.assertEqual(wrapper.__wrapped__, self.func) self.assertEqual(wrapper(0), (0,)) self.assertEqual(wrapper(1), (1,)) diff --git a/tests/test_method.py b/tests/test_cachedmethod.py similarity index 84% rename from tests/test_method.py rename to tests/test_cachedmethod.py index 44dfcaf..924b67f 100644 --- a/tests/test_method.py +++ b/tests/test_cachedmethod.py @@ -1,7 +1,7 @@ import operator import unittest -from cachetools import LRUCache, cachedmethod, keys +from cachetools import LRUCache, _methodkey, cachedmethod, keys class Cached: @@ -125,7 +125,7 @@ def test_weakref(self): import fractions import gc - # in Python 3.4, `int` does not support weak references even + # in Python 3.7, `int` does not support weak references even # when subclassed, but Fraction apparently does... class Int(fractions.Fraction): def __add__(self, other): @@ -185,3 +185,31 @@ def test_unhashable(self): with self.assertRaises(TypeError): cached.get_hashkey(0) + + def test_wrapped(self): + cache = {} + cached = Cached(cache) + + self.assertEqual(len(cache), 0) + self.assertEqual(cached.get.__wrapped__(cached, 0), 0) + self.assertEqual(len(cache), 0) + self.assertEqual(cached.get(0), 1) + self.assertEqual(len(cache), 1) + self.assertEqual(cached.get(0), 1) + self.assertEqual(len(cache), 1) + + def test_attributes(self): + cache = {} + cached = Cached(cache) + + self.assertIs(cached.get.cache(cached), cache) + self.assertIs(cached.get.cache_key, _methodkey) + self.assertIs(cached.get.cache_lock, None) + + def test_attributes_lock(self): + cache = {} + cached = Locked(cache) + + self.assertIs(cached.get.cache(cached), cache) + self.assertIs(cached.get.cache_key, _methodkey) + self.assertIs(cached.get.cache_lock(cached), cached)