Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: tkem/cachetools
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.4.0
Choose a base ref
...
head repository: tkem/cachetools
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.5.0
Choose a head ref
  • 9 commits
  • 14 files changed
  • 2 contributors

Commits on Aug 18, 2024

  1. Fix #292, fix #205, fix #103: TTLCache.expire() returns iterable of e…

    …xpired (key, value) pairs.
    tkem committed Aug 18, 2024
    Copy the full SHA
    c22fc7d View commit details
  2. Copy the full SHA
    7be40f0 View commit details
  3. Update expire docs.

    tkem committed Aug 18, 2024
    Copy the full SHA
    8a38daf View commit details
  4. Bump actions/setup-python from 5.1.0 to 5.1.1

    Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.1.0 to 5.1.1.
    - [Release notes](https://github.com/actions/setup-python/releases)
    - [Commits](actions/setup-python@82c7e63...39cd149)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-python
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored and tkem committed Aug 18, 2024
    Copy the full SHA
    ea158fc View commit details
  5. Fix #302: Improve cachetools.keys unit tests.

    tkem committed Aug 18, 2024
    Copy the full SHA
    e960781 View commit details
  6. Fix #278: Improve TLRUCache docs.

    tkem committed Aug 18, 2024
    Copy the full SHA
    237ad80 View commit details
  7. Format tests with black.

    tkem committed Aug 18, 2024
    Copy the full SHA
    f2ccaca View commit details
  8. Release v5.5.0.

    tkem committed Aug 18, 2024
    Copy the full SHA
    8841efd View commit details
  9. Bump version.

    tkem committed Aug 18, 2024
    Copy the full SHA
    6c78a8f View commit details
Showing with 136 additions and 34 deletions.
  1. +1 −1 .github/workflows/ci.yml
  2. +14 −0 CHANGELOG.rst
  3. +49 −7 docs/index.rst
  4. +17 −6 src/cachetools/__init__.py
  5. +0 −1 tests/__init__.py
  6. +0 −1 tests/test_cache.py
  7. +0 −1 tests/test_fifo.py
  8. +35 −0 tests/test_keys.py
  9. +0 −1 tests/test_lfu.py
  10. +0 −1 tests/test_lru.py
  11. +0 −2 tests/test_mru.py
  12. +0 −1 tests/test_rr.py
  13. +8 −5 tests/test_tlru.py
  14. +12 −7 tests/test_ttl.py
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ jobs:
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "pypy3.9", "pypy3.10"]
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
- uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
14 changes: 14 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
v5.5.0 (2024-08-18)
===================

- ``TTLCache.expire()`` returns iterable of expired ``(key, value)``
pairs.

- ``TLRUCache.expire()`` returns iterable of expired ``(key, value)``
pairs.

- Documentation improvements.

- Update CI environment.


v5.4.0 (2024-07-15)
===================

56 changes: 49 additions & 7 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ method calls.
from unittest import mock
urllib = mock.MagicMock()

import time


Cache implementations
=====================
@@ -153,6 +155,8 @@ computed when the item is inserted into the cache.
items that have expired by the current value returned by
:attr:`timer`.

:returns: An iterable of expired `(key, value)` pairs.

.. autoclass:: TLRUCache(maxsize, ttu, timer=time.monotonic, getsizeof=None)
:members: popitem, timer, ttu

@@ -164,18 +168,29 @@ computed when the item is inserted into the cache.
value of `timer()`.

.. testcode::

from datetime import datetime, timedelta


def my_ttu(_key, value, now):
# assume value.ttl contains the item's time-to-live in hours
return now + timedelta(hours=value.ttl)
# assume value.ttu contains the item's time-to-use in seconds
# note that the _key argument is ignored in this example
return now + value.ttu

cache = TLRUCache(maxsize=10, ttu=my_ttu, timer=datetime.now)
cache = TLRUCache(maxsize=10, ttu=my_ttu)

The expression `ttu(key, value, timer())` defines the expiration
time of a cache item, and must be comparable against later results
of `timer()`.
of `timer()`. As with :class:`TTLCache`, a custom `timer` function
can be supplied, which does not have to return a numeric value.

.. testcode::

from datetime import datetime, timedelta

def datetime_ttu(_key, value, now):
# assume now to be of type datetime.datetime, and
# value.hours to contain the item's time-to-use in hours
return now + timedelta(hours=value.hours)

cache = TLRUCache(maxsize=10, ttu=datetime_ttu, timer=datetime.now)

Items that expire because they have exceeded their time-to-use will
be no longer accessible, and will be removed eventually. If no
@@ -193,6 +208,8 @@ computed when the item is inserted into the cache.
items that have expired by the current value returned by
:attr:`timer`.

:returns: An iterable of expired `(key, value)` pairs.


Extending cache classes
=======================
@@ -217,6 +234,31 @@ cache, this can be achieved by overriding this method in a subclass:
>>> c['c'] = 3
Key "a" evicted with value "1"

With :class:`TTLCache` and :class:`TLRUCache`, items may also be
removed after they expire. In this case, :meth:`popitem` will *not*
be called, but :meth:`expire` will be called from the next mutating
operation and will return an iterable of the expired `(key, value)`
pairs. By overrding :meth:`expire`, a subclass will be able to track
expired items:

.. doctest::
:pyversion: >= 3

>>> class ExpCache(TTLCache):
... def expire(self, time=None):
... items = super().expire(time)
... print(f"Expired items: {items}")
... return items

>>> c = ExpCache(maxsize=10, ttl=1.0)
>>> c['a'] = 1
Expired items: []
>>> c['b'] = 2
Expired items: []
>>> time.sleep(1.5)
>>> c['c'] = 3
Expired items: [('a', 1), ('b', 2)]

Similar to the standard library's :class:`collections.defaultdict`,
subclasses of :class:`Cache` may implement a :meth:`__missing__`
method which is called by :meth:`Cache.__getitem__` if the requested
23 changes: 17 additions & 6 deletions src/cachetools/__init__.py
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
"cachedmethod",
)

__version__ = "5.4.0"
__version__ = "5.5.0"

import collections
import collections.abc
@@ -26,7 +26,6 @@


class _DefaultSize:

__slots__ = ()

def __getitem__(self, _):
@@ -378,7 +377,6 @@ class TTLCache(_TimedCache):
"""LRU Cache implementation with per-item time-to-live (TTL) value."""

class _Link:

__slots__ = ("key", "expires", "next", "prev")

def __init__(self, key=None, expires=None):
@@ -469,19 +467,26 @@ def ttl(self):
return self.__ttl

def expire(self, time=None):
"""Remove expired items from the cache."""
"""Remove expired items from the cache and return an iterable of the
expired `(key, value)` pairs.
"""
if time is None:
time = self.timer()
root = self.__root
curr = root.next
links = self.__links
expired = []
cache_delitem = Cache.__delitem__
cache_getitem = Cache.__getitem__
while curr is not root and not (time < curr.expires):
expired.append((curr.key, cache_getitem(self, curr.key)))
cache_delitem(self, curr.key)
del links[curr.key]
next = curr.next
curr.unlink()
curr = next
return expired

def popitem(self):
"""Remove and return the `(key, value)` pair least recently used that
@@ -508,7 +513,6 @@ class TLRUCache(_TimedCache):

@functools.total_ordering
class _Item:

__slots__ = ("key", "expires", "removed")

def __init__(self, key=None, expires=None):
@@ -583,7 +587,10 @@ def ttu(self):
return self.__ttu

def expire(self, time=None):
"""Remove expired items from the cache."""
"""Remove expired items from the cache and return an iterable of the
expired `(key, value)` pairs.
"""
if time is None:
time = self.timer()
items = self.__items
@@ -592,12 +599,16 @@ def expire(self, time=None):
if len(order) > len(items) * 2:
self.__order = order = [item for item in order if not item.removed]
heapq.heapify(order)
expired = []
cache_delitem = Cache.__delitem__
cache_getitem = Cache.__getitem__
while order and (order[0].removed or not (time < order[0].expires)):
item = heapq.heappop(order)
if not item.removed:
expired.append((item.key, cache_getitem(self, item.key)))
cache_delitem(self, item.key)
del items[item.key]
return expired

def popitem(self):
"""Remove and return the `(key, value)` pair least recently used that
1 change: 0 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@


class CacheTestMixin:

Cache = None

def test_defaults(self):
1 change: 0 additions & 1 deletion tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -6,5 +6,4 @@


class CacheTest(unittest.TestCase, CacheTestMixin):

Cache = cachetools.Cache
1 change: 0 additions & 1 deletion tests/test_fifo.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@


class LRUCacheTest(unittest.TestCase, CacheTestMixin):

Cache = FIFOCache

def test_fifo(self):
35 changes: 35 additions & 0 deletions tests/test_keys.py
Original file line number Diff line number Diff line change
@@ -21,6 +21,24 @@ def test_hashkey(self, key=cachetools.keys.hashkey):
self.assertEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))
self.assertEqual(hash(key(1, 2, 3)), hash(key(1.0, 2.0, 3.0)))

def methodkey(self, key=cachetools.keys.methodkey):
# similar to hashkey(), but ignores its first positional argument
self.assertEqual(key("x"), key("y"))
self.assertEqual(hash(key("x")), hash(key("y")))
self.assertEqual(key("x", 1, 2, 3), key("y", 1, 2, 3))
self.assertEqual(hash(key("x", 1, 2, 3)), hash(key("y", 1, 2, 3)))
self.assertEqual(key("x", 1, 2, 3, x=0), key("y", 1, 2, 3, x=0))
self.assertEqual(hash(key("x", 1, 2, 3, x=0)), hash(key("y", 1, 2, 3, x=0)))
self.assertNotEqual(key("x", 1, 2, 3), key("x", 3, 2, 1))
self.assertNotEqual(key("x", 1, 2, 3), key("x", 1, 2, 3, x=None))
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=None))
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, y=0))
with self.assertRaises(TypeError):
hash("x", key({}))
# untyped keys compare equal
self.assertEqual(key("x", 1, 2, 3), key("y", 1.0, 2.0, 3.0))
self.assertEqual(hash(key("x", 1, 2, 3)), hash(key("y", 1.0, 2.0, 3.0)))

def test_typedkey(self, key=cachetools.keys.typedkey):
self.assertEqual(key(), key())
self.assertEqual(hash(key()), hash(key()))
@@ -37,6 +55,23 @@ def test_typedkey(self, key=cachetools.keys.typedkey):
# typed keys compare unequal
self.assertNotEqual(key(1, 2, 3), key(1.0, 2.0, 3.0))

def test_typedmethodkey(self, key=cachetools.keys.typedmethodkey):
# similar to typedkey(), but ignores its first positional argument
self.assertEqual(key("x"), key("y"))
self.assertEqual(hash(key("x")), hash(key("y")))
self.assertEqual(key("x", 1, 2, 3), key("y", 1, 2, 3))
self.assertEqual(hash(key("x", 1, 2, 3)), hash(key("y", 1, 2, 3)))
self.assertEqual(key("x", 1, 2, 3, x=0), key("y", 1, 2, 3, x=0))
self.assertEqual(hash(key("x", 1, 2, 3, x=0)), hash(key("y", 1, 2, 3, x=0)))
self.assertNotEqual(key("x", 1, 2, 3), key("x", 3, 2, 1))
self.assertNotEqual(key("x", 1, 2, 3), key("x", 1, 2, 3, x=None))
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, x=None))
self.assertNotEqual(key("x", 1, 2, 3, x=0), key("x", 1, 2, 3, y=0))
with self.assertRaises(TypeError):
hash(key("x", {}))
# typed keys compare unequal
self.assertNotEqual(key("x", 1, 2, 3), key("x", 1.0, 2.0, 3.0))

def test_addkeys(self, key=cachetools.keys.hashkey):
self.assertIsInstance(key(), tuple)
self.assertIsInstance(key(1, 2, 3) + key(4, 5, 6), type(key()))
1 change: 0 additions & 1 deletion tests/test_lfu.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@


class LFUCacheTest(unittest.TestCase, CacheTestMixin):

Cache = LFUCache

def test_lfu(self):
1 change: 0 additions & 1 deletion tests/test_lru.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@


class LRUCacheTest(unittest.TestCase, CacheTestMixin):

Cache = LRUCache

def test_lru(self):
2 changes: 0 additions & 2 deletions tests/test_mru.py
Original file line number Diff line number Diff line change
@@ -7,12 +7,10 @@


class MRUCacheTest(unittest.TestCase, CacheTestMixin):

# TODO: method to create cache that can be overridden
Cache = MRUCache

def test_evict__writes_only(self):

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
cache = MRUCache(maxsize=2)
1 change: 0 additions & 1 deletion tests/test_rr.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,6 @@


class RRCacheTest(unittest.TestCase, CacheTestMixin):

Cache = RRCache

def test_rr(self):
13 changes: 8 additions & 5 deletions tests/test_tlru.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,6 @@ def __init__(self, maxsize, ttu=default_ttu, **kwargs):


class TLRUCacheTest(unittest.TestCase, CacheTestMixin):

Cache = TLRUTestCache

def test_ttu(self):
@@ -157,28 +156,32 @@ def test_ttu_expire(self):
self.assertEqual(2, cache[2])
self.assertEqual(3, cache[3])

cache.expire()
items = cache.expire()
self.assertEqual(set(), set(items))
self.assertEqual({1, 2, 3}, set(cache))
self.assertEqual(3, len(cache))
self.assertEqual(1, cache[1])
self.assertEqual(2, cache[2])
self.assertEqual(3, cache[3])

cache.expire(3)
items = cache.expire(3)
self.assertEqual({(1, 1)}, set(items))
self.assertEqual({2, 3}, set(cache))
self.assertEqual(2, len(cache))
self.assertNotIn(1, cache)
self.assertEqual(2, cache[2])
self.assertEqual(3, cache[3])

cache.expire(4)
items = cache.expire(4)
self.assertEqual({(2, 2)}, set(items))
self.assertEqual({3}, set(cache))
self.assertEqual(1, len(cache))
self.assertNotIn(1, cache)
self.assertNotIn(2, cache)
self.assertEqual(3, cache[3])

cache.expire(5)
items = cache.expire(5)
self.assertEqual({(3, 3)}, set(items))
self.assertEqual(set(), set(cache))
self.assertEqual(0, len(cache))
self.assertNotIn(1, cache)
Loading