aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cachetools.py342
-rw-r--r--docs/index.rst46
-rw-r--r--tests/__init__.py126
-rw-r--r--tests/test_cache.py41
-rw-r--r--tests/test_lfucache.py54
-rw-r--r--tests/test_lrucache.py64
-rw-r--r--tests/test_rrcache.py38
-rw-r--r--tests/test_ttlcache.py75
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)