Skip to content

Commit

Permalink
Fix #176: Add cache decorator parameters as attributes.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed May 15, 2022
1 parent 1fd2fde commit 0ace534
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 31 deletions.
33 changes: 14 additions & 19 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,39 +286,35 @@ 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
with urllib.request.urlopen(url) as s:
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
Expand Down Expand Up @@ -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`
Expand Down
8 changes: 8 additions & 0 deletions src/cachetools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
42 changes: 32 additions & 10 deletions tests/test_wrapper.py → tests/test_cached.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import unittest

import cachetools
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -90,14 +87,44 @@ 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)
self.assertEqual(Lock.count, 4)
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):
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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,))
Expand Down
32 changes: 30 additions & 2 deletions tests/test_method.py → tests/test_cachedmethod.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import operator
import unittest

from cachetools import LRUCache, cachedmethod, keys
from cachetools import LRUCache, _methodkey, cachedmethod, keys


class Cached:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)

0 comments on commit 0ace534

Please sign in to comment.