diff options
-rw-r--r-- | cachetools.py | 342 | ||||
-rw-r--r-- | docs/index.rst | 46 | ||||
-rw-r--r-- | tests/__init__.py | 126 | ||||
-rw-r--r-- | tests/test_cache.py | 41 | ||||
-rw-r--r-- | tests/test_lfucache.py | 54 | ||||
-rw-r--r-- | tests/test_lrucache.py | 64 | ||||
-rw-r--r-- | tests/test_rrcache.py | 38 | ||||
-rw-r--r-- | tests/test_ttlcache.py | 75 |
8 files changed, 544 insertions, 242 deletions
diff --git a/cachetools.py b/cachetools.py index ccc42f9..24ef617 100644 --- a/cachetools.py +++ b/cachetools.py @@ -4,157 +4,294 @@ import collections import functools import operator import random +import time try: from threading import RLock except ImportError: from dummy_threading import RLock -__version__ = '0.3.1' +__version__ = '0.4.0' +_marker = object() -class _Cache(collections.MutableMapping): - """Class that wraps a mutable mapping to work as a cache.""" - def __init__(self, mapping, maxsize): - self.__data = mapping - self.__size = sum(map(self.getsizeof, mapping.values()), 0) - self.maxsize = maxsize +class _Link(object): + __slots__ = 'prev', 'next', 'data' + + +class Cache(collections.MutableMapping): + """Mutable mapping to serve as a cache. + + This class discards arbitrary items using :meth:`popitem` to make + space when necessary. Derived classes may override + :meth:`popitem` to implement specific caching strategies. + + """ + + def __init__(self, maxsize, getsizeof=None): + if getsizeof is not None: + self.getsizeof = getsizeof + self.__mapping = dict() + self.__maxsize = maxsize + self.__currsize = 0 def __getitem__(self, key): - return self.__data[key] + return self.__mapping[key][0] def __setitem__(self, key, value): + mapping = self.__mapping + maxsize = self.__maxsize size = self.getsizeof(value) - if size > self.maxsize: - raise ValueError - while self.size > self.maxsize - size: - self.pop(next(iter(self))) - self.__data[key] = value - self.__size += size + if size > maxsize: + raise ValueError('value too large') + if key not in mapping or mapping[key][1] < size: + while self.__currsize + size > maxsize: + self.popitem() + if key in mapping: + self.__currsize -= mapping[key][1] + mapping[key] = (value, size) + self.__currsize += size def __delitem__(self, key): - self.__size -= self.getsizeof(self.__data.pop(key)) + _, size = self.__mapping.pop(key) + self.__currsize -= size def __iter__(self): - return iter(self.__data) + return iter(self.__mapping) def __len__(self): - return len(self.__data) + return len(self.__mapping) def __repr__(self): - return '%s(%r, size=%d, maxsize=%d)' % ( + return '%s(%r, maxsize=%d, currsize=%d)' % ( self.__class__.__name__, - self.__data, - self.__size, + list(self.items()), self.__maxsize, + self.__currsize, ) @property - def size(self): - return self.__size - - @property def maxsize(self): + """Return the maximum size of the cache.""" return self.__maxsize - @maxsize.setter - def maxsize(self, value): - while self.size > value: - self.pop(next(iter(self))) - self.__maxsize = value + @property + def currsize(self): + """Return the current size of the cache.""" + return self.__currsize @staticmethod - def getsizeof(_): + def getsizeof(object): + """Return the size of a cache element.""" return 1 -class LRUCache(_Cache): - """Least Recently Used (LRU) cache implementation. +class RRCache(Cache): + """Random Replacement (RR) cache implementation. - Discards the least recently used items first to make space when - necessary. + This cache randomly selects candidate items and discards them to + make space when necessary. - This implementation uses :class:`collections.OrderedDict` to keep - track of item usage. """ - class OrderedDict(collections.OrderedDict): - # OrderedDict.move_to_end is only available in Python 3 - if hasattr(collections.OrderedDict, 'move_to_end'): - def __getitem__(self, key, - getitem=collections.OrderedDict.__getitem__): - self.move_to_end(key) - return getitem(self, key) - else: - def __getitem__(self, key, - getitem=collections.OrderedDict.__getitem__, - delitem=collections.OrderedDict.__delitem__, - setitem=collections.OrderedDict.__setitem__): - value = getitem(self, key) - delitem(self, key) - setitem(self, key, value) - return value - - def __init__(self, maxsize, getsizeof=None): - if getsizeof is not None: - self.getsizeof = getsizeof - _Cache.__init__(self, self.OrderedDict(), maxsize) + def popitem(self): + """Remove and return a random `(key, value)` pair.""" + try: + key = random.choice(list(self)) + except IndexError: + raise KeyError('cache is empty') + return (key, self.pop(key)) -class LFUCache(_Cache): +class LFUCache(Cache): """Least Frequently Used (LFU) cache implementation. - Counts how often an item is needed, and discards the items used - least often to make space when necessary. + This cache counts how often an item is retrieved, and discards the + items used least often to make space when necessary. - This implementation uses :class:`collections.Counter` to keep - track of usage counts. """ def __init__(self, maxsize, getsizeof=None): if getsizeof is not None: - self.getsizeof = getsizeof - _Cache.__init__(self, {}, maxsize) - self.__counter = collections.Counter() + Cache.__init__(self, maxsize, lambda e: getsizeof(e[0])) + else: + Cache.__init__(self, maxsize) - def __getitem__(self, key): - value = _Cache.__getitem__(self, key) - self.__counter[key] += 1 - return value + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + entry = cache_getitem(self, key) + entry[1] += 1 + return entry[0] - def __setitem__(self, key, value): - _Cache.__setitem__(self, key, value) - self.__counter[key] += 0 + def __setitem__(self, key, value, cache_setitem=Cache.__setitem__): + cache_setitem(self, key, [value, 0]) - def __delitem__(self, key): - _Cache.__delitem__(self, key) - del self.__counter[key] - - def __iter__(self): - items = reversed(self.__counter.most_common()) - return iter(map(operator.itemgetter(0), items)) + def popitem(self): + """Remove and return the `(key, value)` pair least frequently used.""" + items = ((key, Cache.__getitem__(self, key)[1]) for key in self) + try: + key, _ = min(items, key=operator.itemgetter(1)) + except ValueError: + raise KeyError('cache is empty') + return (key, self.pop(key)) -class RRCache(_Cache): - """Random Replacement (RR) cache implementation. +class LRUCache(Cache): + """Least Recently Used (LRU) cache implementation. - Randomly selects candidate items and discards then to make space - when necessary. + This cache discards the least recently used items first to make + space when necessary. - This implementations uses :func:`random.shuffle` to select the - items to be discarded. """ def __init__(self, maxsize, getsizeof=None): if getsizeof is not None: - self.getsizeof = getsizeof - _Cache.__init__(self, {}, maxsize) + Cache.__init__(self, maxsize, lambda e: getsizeof(e[0])) + else: + Cache.__init__(self, maxsize) + root = _Link() + root.prev = root.next = root + self.__root = root + + def __getitem__(self, key, cache_getitem=Cache.__getitem__): + value, link = cache_getitem(self, key) + root = self.__root + link.prev.next = link.next + link.next.prev = link.prev + link.prev = tail = root.prev + link.next = root + tail.next = root.prev = link + return value - def __iter__(self): - keys = list(_Cache.__iter__(self)) - random.shuffle(keys) - return iter(keys) + def __setitem__(self, key, value, + cache_getitem=Cache.__getitem__, + cache_setitem=Cache.__setitem__): + try: + _, link = cache_getitem(self, key) + except KeyError: + link = _Link() + cache_setitem(self, key, (value, link)) + try: + link.prev.next = link.next + link.next.prev = link.prev + except AttributeError: + link.data = key + root = self.__root + link.prev = tail = root.prev + link.next = root + tail.next = root.prev = link + + def __delitem__(self, key, + cache_getitem=Cache.__getitem__, + cache_delitem=Cache.__delitem__): + _, link = cache_getitem(self, key) + cache_delitem(self, key) + link.prev.next = link.next + link.next.prev = link.prev + del link.next + del link.prev + + def popitem(self): + """Remove and return the `(key, value)` pair least recently used.""" + root = self.__root + link = root.next + if link is root: + raise KeyError('cache is empty') + key = link.data + return (key, self.pop(key)) + + +class TTLCache(LRUCache): + """LRU cache implementation with per-item time-to-live (TTL) value. + + This least-recently-used cache associates a time-to-live value + with each item. Items that expire because they have exceeded + their time-to-live are removed from the cache automatically. + + """ + + def __init__(self, maxsize, ttl, getsizeof=None, timer=time.time): + if getsizeof is not None: + LRUCache.__init__(self, maxsize, lambda e: getsizeof(e[0])) + else: + LRUCache.__init__(self, maxsize) + root = _Link() + root.prev = root.next = root + self.__root = root + self.__timer = timer + self.__ttl = ttl + + def __getitem__(self, key, + cache_getitem=LRUCache.__getitem__, + cache_delitem=LRUCache.__delitem__): + value, link = cache_getitem(self, key) + if self.__timer() < link.data[1]: + return value + root = self.__root + head = root.next + link = link.next + while head is not link: + cache_delitem(self, head.data[0]) + head.next.prev = root + head = root.next = head.next + raise KeyError('%r has expired' % key) + + def __setitem__(self, key, value, + cache_getitem=LRUCache.__getitem__, + cache_setitem=LRUCache.__setitem__, + cache_delitem=LRUCache.__delitem__): + root = self.__root + head = root.next + time = self.__timer() + while head is not root and head.data[1] < time: + cache_delitem(self, head.data[0]) + head.next.prev = root + head = root.next = head.next + try: + _, link = cache_getitem(self, key) + except KeyError: + link = _Link() + cache_setitem(self, key, (value, link)) + try: + link.prev.next = link.next + link.next.prev = link.prev + except AttributeError: + pass + link.data = (key, time + self.__ttl) + link.prev = tail = root.prev + link.next = root + tail.next = root.prev = link + + def __delitem__(self, key, + cache_getitem=LRUCache.__getitem__, + cache_delitem=LRUCache.__delitem__): + _, link = cache_getitem(self, key) + cache_delitem(self, key) + link.prev.next = link.next + link.next.prev = link.prev + + def __repr__(self, cache_getitem=LRUCache.__getitem__): + return '%s(%r, maxsize=%d, currsize=%d)' % ( + self.__class__.__name__, + [(key, cache_getitem(self, key)[0]) for key in self], + self.maxsize, + self.currsize, + ) + + def pop(self, key, default=_marker): + try: + value, link = LRUCache.__getitem__(self, key) + except KeyError: + if default is _marker: + raise + else: + return default + LRUCache.__delitem__(self, key) + link.prev.next = link.next + link.next.prev = link.prev + del link.next + del link.prev + return value CacheInfo = collections.namedtuple('CacheInfo', 'hits misses maxsize currsize') @@ -191,7 +328,10 @@ def _cachedfunc(cache, makekey, lock): def cache_info(): with lock: - return CacheInfo(stats[0], stats[1], cache.maxsize, cache.size) + hits, misses = stats + maxsize = cache.maxsize + currsize = cache.currsize + return CacheInfo(hits, misses, maxsize, currsize) def cache_clear(): with lock: @@ -204,19 +344,17 @@ def _cachedfunc(cache, makekey, lock): return decorator -def _cachedmeth(getcache, makekey, lock): +def _cachedmeth(getcache, makekey): def decorator(func): def wrapper(self, *args, **kwargs): key = makekey((func,) + args, kwargs) cache = getcache(self) - with lock: - try: - return cache[key] - except KeyError: - pass + try: + return cache[key] + except KeyError: + pass result = func(self, *args, **kwargs) - with lock: - cache[key] = result + cache[key] = result return result return functools.update_wrapper(wrapper, func) @@ -254,10 +392,10 @@ def rr_cache(maxsize=128, typed=False, getsizeof=None, lock=RLock): return _cachedfunc(RRCache(maxsize, getsizeof), makekey, lock()) -def cachedmethod(getcache, typed=False, lock=RLock): +def cachedmethod(cache, typed=False): """Decorator to wrap a class or instance method with a memoizing callable that saves results in a (possibly shared) cache. """ makekey = _makekey_typed if typed else _makekey - return _cachedmeth(getcache, makekey, lock()) + return _cachedmeth(cache, makekey) diff --git a/docs/index.rst b/docs/index.rst index 0c78a17..c0ad084 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ including a variant of the Python 3 Standard Library >>> cache['first'] = 1 >>> cache['second'] = 2 >>> cache - LRUCache(OrderedDict([('first', 1), ('second', 2)]), size=2, maxsize=2) + LRUCache(maxsize=2, currsize=2, items=[('first', 1)]) >>> cache['third'] = 3 >>> cache LRUCache(OrderedDict([('second', 2), ('third', 3)]), size=2, maxsize=2) @@ -26,26 +26,30 @@ including a variant of the Python 3 Standard Library >>> cache LRUCache(OrderedDict([('second', 2), ('fourth', 4)]), size=2, maxsize=2) -For the purpose of this module, a *cache* is a mutable_ mapping_ with -additional attributes :attr:`size` and :attr:`maxsize`, which hold the -current and maximum size of the cache, and a (possibly static) method -:meth:`getsizeof`. +For the purpose of this module, a *cache* is a mutable_ mapping_ of a +fixed maximum *size*. When the cache is full, i.e. the current size +of the cache exceeds its maximum size, the cache must choose which +item(s) to discard based on a suitable `cache algorithm`_. -The current size of the cache is the sum of the results of -:meth:`getsizeof` applied to each of the cache's values, -i.e. ``cache.size == sum(map(cache.getsizeof, cache.values()), 0)``. -As a special case, if :meth:`getsizeof` returns :const:`1` -irrespective of its argument, ``cache.size == len(cache)``. - -When the cache is full, i.e. ``cache.size > cache.maxsize``, the cache -must choose which item(s) to discard based on a suitable `cache -algorithm`_. +In general, a cache's size is the sum of its element's sizes. For the +trivial case, if the size of each element is :const:`1`, a cache's +size is equal to the number of its entries, i.e. :func:`len`. An +element's size may also be a property or function of its value, +e.g. the result of :func:`sys.getsizeof`, or :func:`len` for string +and sequence elements. This module provides various cache implementations based on different cache algorithms, as well as decorators for easily memoizing function and method calls. +Cache Base Class +------------------------------------------------------------------------ + +.. autoclass:: Cache + :members: + + Cache Implementations ------------------------------------------------------------------------ @@ -58,6 +62,18 @@ Cache Implementations .. autoclass:: RRCache :members: +.. autoclass:: TTLCache + :members: + + Note that a cache element may expire at *any* time, so the + following *may* raise an exception:: + + cache = TTLCache(100, 1) + ... + for k in cache: + print(cache[k]) + + Function Decorators ------------------------------------------------------------------------ @@ -126,7 +142,7 @@ Method Decorators Python 3 example of a shared (class) LRU cache for static web content:: - class CachedPEPs(object): + class CachedPEPs(object): cache = LRUCache(maxsize=32) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..4ab4a11 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,126 @@ +class CacheTestMixin(object): + + def make_cache(self, maxsize, getsizeof=None): + raise NotImplementedError + + def test_insert(self): + cache = self.make_cache(maxsize=2) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + cache[3] = 3 + self.assertEqual(2, len(cache)) + self.assertEqual(3, cache[3]) + self.assertTrue(1 in cache or 2 in cache) + + cache[4] = 4 + self.assertEqual(2, len(cache)) + self.assertEqual(4, cache[4]) + self.assertTrue(1 in cache or 2 in cache or 3 in cache) + + def test_update(self): + cache = self.make_cache(maxsize=2) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + cache.update({1: 'a', 2: 'b'}) + self.assertEqual(2, len(cache)) + self.assertEqual('a', cache[1]) + self.assertEqual('b', cache[2]) + + def test_delete(self): + cache = self.make_cache(maxsize=2) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + del cache[2] + self.assertEqual(1, len(cache)) + self.assertEqual(1, cache[1]) + self.assertNotIn(2, cache) + + del cache[1] + self.assertEqual(0, len(cache)) + self.assertNotIn(1, cache) + self.assertNotIn(2, cache) + + def test_pop(self): + cache = self.make_cache(maxsize=2) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, cache.pop(2)) + self.assertEqual(1, len(cache)) + self.assertEqual(1, cache.pop(1)) + self.assertEqual(0, len(cache)) + + with self.assertRaises(KeyError): + cache.pop(2) + with self.assertRaises(KeyError): + cache.pop(1) + with self.assertRaises(KeyError): + cache.pop(0) + + self.assertEqual(None, cache.pop(2, None)) + self.assertEqual(None, cache.pop(1, None)) + self.assertEqual(None, cache.pop(0, None)) + + def test_popitem(self): + cache = self.make_cache(maxsize=2) + + cache.update({1: 1, 2: 2}) + self.assertIn(cache.pop(1), {1: 1, 2: 2}) + self.assertEqual(1, len(cache)) + self.assertIn(cache.pop(2), {1: 1, 2: 2}) + self.assertEqual(0, len(cache)) + + with self.assertRaises(KeyError): + cache.popitem() + + def test_getsizeof(self): + cache = self.make_cache(maxsize=3, getsizeof=lambda x: x) + self.assertEqual(3, cache.maxsize) + self.assertEqual(0, cache.currsize) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(3, cache.currsize) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + cache[1] = 2 + self.assertEqual(1, len(cache)) + self.assertEqual(2, cache.currsize) + self.assertEqual(2, cache[1]) + self.assertNotIn(2, cache) + + cache.update({1: 1, 2: 2}) + self.assertEqual(2, len(cache)) + self.assertEqual(3, cache.currsize) + self.assertEqual(1, cache[1]) + self.assertEqual(2, cache[2]) + + cache[3] = 3 + self.assertEqual(1, len(cache)) + self.assertEqual(3, cache.currsize) + self.assertEqual(3, cache[3]) + self.assertNotIn(1, cache) + self.assertNotIn(2, cache) + + with self.assertRaises(ValueError): + cache[4] = 4 + self.assertEqual(1, len(cache)) + self.assertEqual(3, cache.currsize) + self.assertEqual(3, cache[3]) diff --git a/tests/test_cache.py b/tests/test_cache.py index e2a58e6..60e9220 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,41 +1,10 @@ import unittest -import cachetools -import collections +from . import CacheTestMixin +from cachetools import Cache -class CacheTest(unittest.TestCase): +class CacheTest(unittest.TestCase, CacheTestMixin): - def test_dict_cache(self): - cache = cachetools._Cache({'a': 1, 'b': 2}, maxsize=2) - - self.assertEqual(len(cache), 2) - self.assertEqual(cache['a'], 1) - self.assertEqual(cache['b'], 2) - - cache['c'] = 3 - - self.assertEqual(len(cache), 2) - self.assertTrue('a' in cache or 'b' in cache) - self.assertEqual(cache['c'], 3) - - cache.maxsize = 1 - - self.assertEqual(len(cache), 1) - self.assertTrue('a' in cache or 'b' in cache or 'c' in cache) - - def test_ordered_dict_cache(self): - cache = cachetools._Cache(collections.OrderedDict(), maxsize=2) - - cache['a'] = 1 - cache['b'] = 2 - cache['c'] = 3 - - self.assertEqual(len(cache), 2) - self.assertEqual(cache['b'], 2) - self.assertEqual(cache['c'], 3) - - cache.maxsize = 1 - - self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) + def make_cache(self, maxsize, getsizeof=None): + return Cache(maxsize, getsizeof) diff --git a/tests/test_lfucache.py b/tests/test_lfucache.py index cfdf0ec..245793e 100644 --- a/tests/test_lfucache.py +++ b/tests/test_lfucache.py @@ -1,5 +1,6 @@ import unittest +from . import CacheTestMixin from cachetools import LFUCache, lfu_cache @@ -13,47 +14,50 @@ def cached_typed(n): return n -class LFUCacheTest(unittest.TestCase): +class LFUCacheTest(unittest.TestCase, CacheTestMixin): - def test_insert(self): - cache = LFUCache(maxsize=2) + def make_cache(self, maxsize, getsizeof=None): + return LFUCache(maxsize, getsizeof) - cache['a'] = 1 - cache['a'] - cache['b'] = 2 - cache['c'] = 3 + def test_lfu_insert(self): + cache = self.make_cache(maxsize=2) + + cache[1] = 1 + cache[1] + cache[2] = 2 + cache[3] = 3 self.assertEqual(len(cache), 2) - self.assertEqual(cache['a'], 1) - self.assertTrue('b' in cache or 'c' in cache) - self.assertTrue('b' not in cache or 'c' not in cache) + self.assertEqual(cache[1], 1) + self.assertTrue(2 in cache or 3 in cache) + self.assertTrue(2 not in cache or 3 not in cache) - cache['d'] = 4 + cache[4] = 4 self.assertEqual(len(cache), 2) - self.assertEqual(cache['d'], 4) - self.assertEqual(cache['a'], 1) + self.assertEqual(cache[4], 4) + self.assertEqual(cache[1], 1) - def test_getsizeof(self): - cache = LFUCache(maxsize=3, getsizeof=lambda x: x) + def test_lfu_getsizeof(self): + cache = self.make_cache(maxsize=3, getsizeof=lambda x: x) - cache['a'] = 1 - cache['b'] = 2 + cache[1] = 1 + cache[2] = 2 self.assertEqual(len(cache), 2) - self.assertEqual(cache['a'], 1) - self.assertEqual(cache['b'], 2) + self.assertEqual(cache[1], 1) + self.assertEqual(cache[2], 2) - cache['c'] = 3 + cache[3] = 3 self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) - self.assertNotIn('a', cache) - self.assertNotIn('b', cache) + self.assertEqual(cache[3], 3) + self.assertNotIn(1, cache) + self.assertNotIn(2, cache) with self.assertRaises(ValueError): - cache['d'] = 4 + cache[4] = 4 self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) + self.assertEqual(cache[3], 3) def test_decorator(self): self.assertEqual(cached(1), 1) diff --git a/tests/test_lrucache.py b/tests/test_lrucache.py index c8487fe..242a0e7 100644 --- a/tests/test_lrucache.py +++ b/tests/test_lrucache.py @@ -1,5 +1,6 @@ import unittest +from . import CacheTestMixin from cachetools import LRUCache, lru_cache @@ -13,54 +14,57 @@ def cached_typed(n): return n -class LRUCacheTest(unittest.TestCase): +class LRUCacheTest(unittest.TestCase, CacheTestMixin): - def test_insert(self): - cache = LRUCache(maxsize=2) + def make_cache(self, maxsize, getsizeof=None): + return LRUCache(maxsize, getsizeof) - cache['a'] = 1 - cache['b'] = 2 - cache['c'] = 3 + def test_lru_insert(self): + cache = self.make_cache(maxsize=2) + + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 self.assertEqual(len(cache), 2) - self.assertEqual(cache['b'], 2) - self.assertEqual(cache['c'], 3) - self.assertNotIn('a', cache) + self.assertEqual(cache[2], 2) + self.assertEqual(cache[3], 3) + self.assertNotIn(1, cache) - cache['b'] - cache['d'] = 4 + cache[2] + cache[4] = 4 self.assertEqual(len(cache), 2) - self.assertEqual(cache['b'], 2) - self.assertEqual(cache['d'], 4) - self.assertNotIn('c', cache) + self.assertEqual(cache[2], 2) + self.assertEqual(cache[4], 4) + self.assertNotIn(3, cache) - cache['e'] = 5 + cache[5] = 5 self.assertEqual(len(cache), 2) - self.assertEqual(cache['d'], 4) - self.assertEqual(cache['e'], 5) - self.assertNotIn('b', cache) + self.assertEqual(cache[4], 4) + self.assertEqual(cache[5], 5) + self.assertNotIn(2, cache) - def test_getsizeof(self): - cache = LRUCache(maxsize=3, getsizeof=lambda x: x) + def test_lru_getsizeof(self): + cache = self.make_cache(maxsize=3, getsizeof=lambda x: x) - cache['a'] = 1 - cache['b'] = 2 + cache[1] = 1 + cache[2] = 2 self.assertEqual(len(cache), 2) - self.assertEqual(cache['a'], 1) - self.assertEqual(cache['b'], 2) + self.assertEqual(cache[1], 1) + self.assertEqual(cache[2], 2) - cache['c'] = 3 + cache[3] = 3 self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) - self.assertNotIn('a', cache) - self.assertNotIn('b', cache) + self.assertEqual(cache[3], 3) + self.assertNotIn(1, cache) + self.assertNotIn(2, cache) with self.assertRaises(ValueError): - cache['d'] = 4 + cache[4] = 4 self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) + self.assertEqual(cache[3], 3) def test_decorator(self): self.assertEqual(cached.cache_info(), (0, 0, 2, 0)) diff --git a/tests/test_rrcache.py b/tests/test_rrcache.py index 088eedf..207b7fd 100644 --- a/tests/test_rrcache.py +++ b/tests/test_rrcache.py @@ -1,5 +1,6 @@ import unittest +from . import CacheTestMixin from cachetools import RRCache, rr_cache @@ -13,41 +14,10 @@ def cached_typed(n): return n -class RRCacheTest(unittest.TestCase): +class RRCacheTest(unittest.TestCase, CacheTestMixin): - def test_insert(self): - cache = RRCache(maxsize=2) - - cache['a'] = 1 - cache['b'] = 2 - cache['c'] = 3 - - self.assertEqual(len(cache), 2) - self.assertTrue('a' in cache or ('b' in cache and 'c' in cache)) - self.assertTrue('b' in cache or ('a' in cache and 'c' in cache)) - self.assertTrue('c' in cache or ('a' in cache and 'b' in cache)) - - def test_getsizeof(self): - cache = RRCache(maxsize=3, getsizeof=lambda x: x) - - cache['a'] = 1 - cache['b'] = 2 - - self.assertEqual(len(cache), 2) - self.assertEqual(cache['a'], 1) - self.assertEqual(cache['b'], 2) - - cache['c'] = 3 - - self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) - self.assertNotIn('a', cache) - self.assertNotIn('b', cache) - - with self.assertRaises(ValueError): - cache['d'] = 4 - self.assertEqual(len(cache), 1) - self.assertEqual(cache['c'], 3) + def make_cache(self, maxsize, getsizeof=None): + return RRCache(maxsize, getsizeof) def test_decorator(self): self.assertEqual(cached(1), 1) diff --git a/tests/test_ttlcache.py b/tests/test_ttlcache.py new file mode 100644 index 0000000..d1f946c --- /dev/null +++ b/tests/test_ttlcache.py @@ -0,0 +1,75 @@ +import unittest + +from . import CacheTestMixin +from cachetools import TTLCache + + +class TTLCacheTest(unittest.TestCase, CacheTestMixin): + + def make_cache(self, maxsize, getsizeof=None, ttl=86400): + return TTLCache(maxsize, ttl, getsizeof) + + def test_ttl_insert(self): + cache = self.make_cache(maxsize=2) + + cache[1] = 1 + cache[2] = 2 + #cache[1] = 1 + cache[3] = 3 + + self.assertEqual(len(cache), 2) + #self.assertEqual(cache[1], 1) + #self.assertTrue(2 in cache or 3 in cache) + #self.assertTrue(2 not in cache or 3 not in cache) + + cache[4] = 4 + self.assertEqual(len(cache), 2) + self.assertEqual(cache[4], 4) + #self.assertEqual(cache[1], 1) + + def test_ttl_getsizeof(self): + cache = self.make_cache(maxsize=3, getsizeof=lambda x: x) + + cache[1] = 1 + cache[2] = 2 + + self.assertEqual(len(cache), 2) + self.assertEqual(cache[1], 1) + self.assertEqual(cache[2], 2) + + cache[3] = 3 + + self.assertEqual(len(cache), 1) + self.assertEqual(cache[3], 3) + self.assertNotIn(1, cache) + self.assertNotIn(2, cache) + + with self.assertRaises(ValueError): + cache[4] = 4 + self.assertEqual(len(cache), 1) + self.assertEqual(cache[3], 3) + + def test_ttl_expire(self): + cache = self.make_cache(maxsize=2, ttl=0) + + cache[1] = 1 + cache[2] = 2 + cache[3] = 3 + + with self.assertRaises(KeyError): + cache[1] + with self.assertRaises(KeyError): + cache[2] + with self.assertRaises(KeyError): + cache[3] + +# +# self.assertEqual(len(cache), 2) +# self.assertEqual(cache[1], 1) +# self.assertTrue(2 in cache or 3 in cache) +# self.assertTrue(2 not in cache or 3 not in cache) +# +# cache[4] = 4 +# self.assertEqual(len(cache), 2) +# self.assertEqual(cache[4], 4) +# self.assertEqual(cache[1], 1) |