Skip to content

Commit

Permalink
Merge branch 'dargueta-mru-cache' into dev/4.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Aug 18, 2020
2 parents 31a78b5 + 55d67e6 commit 49629b9
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 1 deletion.
2 changes: 2 additions & 0 deletions cachetools/__init__.py
Expand Up @@ -4,13 +4,15 @@
from .decorators import cached, cachedmethod
from .lfu import LFUCache
from .lru import LRUCache
from .mru import MRUCache
from .rr import RRCache
from .ttl import TTLCache

__all__ = (
'Cache',
'LFUCache',
'LRUCache',
'MRUCache',
'RRCache',
'TTLCache',
'cached',
Expand Down
16 changes: 15 additions & 1 deletion cachetools/func.py
Expand Up @@ -14,10 +14,11 @@
from . import keys
from .lfu import LFUCache
from .lru import LRUCache
from .mru import MRUCache
from .rr import RRCache
from .ttl import TTLCache

__all__ = ('lfu_cache', 'lru_cache', 'rr_cache', 'ttl_cache')
__all__ = ('lfu_cache', 'lru_cache', 'mru_cache', 'rr_cache', 'ttl_cache')


_CacheInfo = collections.namedtuple('CacheInfo', [
Expand Down Expand Up @@ -120,6 +121,19 @@ def lru_cache(maxsize=128, typed=False):
return _cache(LRUCache(maxsize), typed)


def mru_cache(maxsize=128, typed=False):
"""Decorator to wrap a function with a memoizing callable that saves
up to `maxsize` results based on a Most Recently Used (MRU)
algorithm.
"""
if maxsize is None:
return _cache(_UnboundCache(), typed)
elif callable(maxsize):
return _cache(MRUCache(128), typed)(maxsize)
else:
return _cache(MRUCache(maxsize), typed)


def rr_cache(maxsize=128, choice=random.choice, typed=False):
"""Decorator to wrap a function with a memoizing callable that saves
up to `maxsize` results based on a Random Replacement (RR)
Expand Down
38 changes: 38 additions & 0 deletions cachetools/mru.py
@@ -0,0 +1,38 @@
import collections

from cachetools.cache import Cache


class MRUCache(Cache):
"""Most Recently Used (MRU) cache implementation."""

def __init__(self, maxsize, getsizeof=None):
super().__init__(maxsize, getsizeof)
self.__order = collections.OrderedDict()

def __getitem__(self, key):
value = super().__getitem__(key)
self.__update(key)
return value

def __setitem__(self, key, value):
super().__setitem__(key, value)
self.__update(key)

def __delitem__(self, key):
super().__delitem__(key)
del self.__order[key]

def popitem(self):
"""Remove and return the `(key, value)` pair most recently used."""
if not self.__order:
raise KeyError(type(self).__name__ + ' cache is empty') from None

key = next(iter(self.__order))
return (key, self.pop(key))

def __update(self, key):
try:
self.__order.move_to_end(key, last=False)
except KeyError:
self.__order[key] = None
5 changes: 5 additions & 0 deletions tests/test_func.py
Expand Up @@ -99,6 +99,11 @@ class LRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):
DECORATOR = staticmethod(cachetools.func.lru_cache)


class MRUDecoratorTest(unittest.TestCase, DecoratorTestMixin):

DECORATOR = staticmethod(cachetools.func.mru_cache)


class RRDecoratorTest(unittest.TestCase, DecoratorTestMixin):

DECORATOR = staticmethod(cachetools.func.rr_cache)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_mru.py
@@ -0,0 +1,50 @@
import unittest

from cachetools import MRUCache

from . import CacheTestMixin


class MRUCacheTest(unittest.TestCase, CacheTestMixin):

Cache = MRUCache

def test_evict__writes_only(self):
cache = MRUCache(maxsize=2)

cache[1] = 1
cache[2] = 2
cache[3] = 3 # Evicts 1 because nothing's been used yet

assert len(cache) == 2
assert 1 not in cache, 'Wrong key was evicted. Should have been `1`.'
assert 2 in cache
assert 3 in cache

def test_evict__with_access(self):
cache = MRUCache(maxsize=2)

cache[1] = 1
cache[2] = 2
cache[1]
cache[2]
cache[3] = 3 # Evicts 2
assert 2 not in cache, 'Wrong key was evicted. Should have been `2`.'
assert 1 in cache
assert 3 in cache

def test_evict__with_delete(self):
cache = MRUCache(maxsize=2)

cache[1] = 1
cache[2] = 2
del cache[2]
cache[3] = 3 # Doesn't evict anything because we just deleted 2

assert 2 not in cache
assert 1 in cache

cache[4] = 4 # Should evict 1 as we just accessed it with __contains__
assert 1 not in cache
assert 3 in cache
assert 4 in cache

0 comments on commit 49629b9

Please sign in to comment.