diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 123c11ac28737e79ef645058ca6b773f6827243f..0cc4dabc4521544b65fb56a788c02018a60da80e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,18 @@ This document contains the ``fslpy`` release history in reverse chronological order. +2.6.0 (Under development) +------------------------- + + +Changed +^^^^^^^ + + +* The :class:`.Cache` class has a new ``lru`` option, allowing it to be used + as a least-recently-used cache. + + 2.5.0 (Tuesday 6th August 2019) ------------------------------- diff --git a/fsl/utils/cache.py b/fsl/utils/cache.py index 5a89817d99520d83b3a8d3729b9dd117e0cb0be9..cf50a50aa281f1d8ad8b27e06160ecfe16598085 100644 --- a/fsl/utils/cache.py +++ b/fsl/utils/cache.py @@ -42,14 +42,20 @@ class Cache(object): raised. """ - def __init__(self, maxsize=100): + def __init__(self, maxsize=100, lru=False): """Create a ``Cache``. :arg maxsize: Maximum number of items allowed in the ``Cache`` before it starts dropping old items + + :arg lru: (least recently used) If ``False`` (the default), items + are dropped according to their insertion time. Otherwise, + items are dropped according to their most recent access + time. """ self.__cache = collections.OrderedDict() self.__maxsize = maxsize + self.__lru = lru def put(self, key, value, expiry=0): @@ -94,14 +100,26 @@ class Cache(object): else: entry = self.__cache[key] + # Check to see if the entry + # has expired + now = time.time() + if entry.expiry > 0: - if time.time() - entry.storetime > entry.expiry: + if now - entry.storetime > entry.expiry: self.__cache.pop(key) if defaultSpecified: return default else: raise Expired(key) + # If we are an lru cache, update + # this entry's expiry, and update + # its order in the cache dict + if self.__lru: + entry.storetime = now + self.__cache.pop(key) + self.__cache[key] = entry + return entry.value @@ -125,6 +143,13 @@ class Cache(object): return self.put(key, value) + def __contains__(self, key): + """Check whether an item is in the cache. Note that the item may + be in the cache, but it may be expired. + """ + return key in self.__cache + + def __parseDefault(self, *args, **kwargs): """Used by the :meth:`get` method. Parses the ``default`` argument, which may be specified as either a positional or keyword argumnet. @@ -134,7 +159,7 @@ class Cache(object): - ``True`` if a default argument was specified, ``False`` otherwise. - - The specifeid default value, or ``None`` if it wasn't + - The specified default value, or ``None`` if it wasn't specified. """ diff --git a/tests/test_cache.py b/tests/test_cache.py index cb643ae43291043e9fa643358c6acac289c1af86..4dcc970323d2b2e2e2c81a5a05ededb796918516 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -113,7 +113,36 @@ def test_expiry(): with pytest.raises(cache.Expired): c.get(0) + with pytest.raises(cache.Expired): + c.get(1) + assert c.get(1, default='default') == 'default' # And that the cache is empty assert len(c) == 0 + + +def test_lru(): + c = cache.Cache(maxsize=3, lru=True) + + c[0] = '0' + c[1] = '1' + c[2] = '2' + c[3] = '3' + + # normal behaviour - first inserted + # is dropped + with pytest.raises(KeyError): + assert c.get(0) + + # lru behaviour - oldest accessed is + # dropped + c[1] + c[4] = '4' + with pytest.raises(KeyError): + c[2] + + c[1] + c[3] + c[4] + assert len(c) == 3