spulec/freezegun

Does not mock time for TTLCache

irusland opened this issue · 5 comments

Hey, I've recently encountered the problem with freezing the time in TTLCache

Steps to reproduce

import time
from datetime import timedelta

from cachetools import TTLCache
from freezegun import freeze_time

with freeze_time() as frozen_datetime:
    ttl = TTLCache(
        maxsize=10, ttl=1,
        # timer=time.monotonic,  # uncomment this to pass assertion
    )
    ttl.update({'1': 1})
    assert ttl.keys()
    frozen_datetime.tick(timedelta(seconds=10000))
    assert not ttl.keys()

The problem with that is when use reference as a function parameter it caches timer=time.monotonic in defaults so freeze gun does not override it

Is there a way to overcome this?

Ive come up with the solution as overriding the standard ttlcache like so

import time

from cachetools import TTLCache as TTLCacheBase


class TTLCache(TTLCacheBase):
    def __init__(self, maxsize, ttl, timer=None, getsizeof=None):
        if timer is None:
            timer = time.monotonic
        super().__init__(maxsize=maxsize, ttl=ttl, timer=timer, getsizeof=getsizeof)

Same:

In [1]: from freezegun import freeze_time
In [2]: from datetime import datetime, timedelta
In [3]: from cachetools.func import ttl_cache
In [4]: @ttl_cache(ttl=100)
   ...: def now():
   ...:     return datetime.now()
In [5]: import time
In [6]: timer = time.monotonic
In [7]: print(timer())
   ...: print(now())
   ...: with freeze_time(timedelta(days=4), tick=True):
   ...:     print(timer())
   ...:     print(now())
   ...:     print(timer())
61.09706311
2022-11-17 12:51:30.917532
1669027890.946961
2022-11-17 12:51:30.917532
1669027890.947039
ylhan commented

+1 I'm running into this too. It's making ttl_cache impossible to test elegantly.

to solve this cachetools would have to stop memoizing pre-patching time functions in the factories

https://github.com/tkem/cachetools/blob/d991ac71b4eb6394be5ec572b835434081393215/src/cachetools/func.py#L107

I hit this same issue today, and found that a relatively simple fix was to create a timer function that returns the result of time.monotonic() - this way, freezegun's patching seems to work as expected

def mytimer():
    return time.monotonic()
    
@cached(cache=TTLCache(maxsize=128, ttl=300, timer=mytimer))
def my_cached_function():
    etc...

Once I did that all the tests that were supposed to pass suddenly passed!

Looking at @POD666's test, I turned it into a script as follows:

from freezegun import freeze_time
from datetime import datetime, timedelta
from cachetools.func import ttl_cache
import time

def mytimer():
    return time.monotonic()

@ttl_cache(ttl=100, timer=mytimer)
def now():
    return datetime.now()


timer = time.monotonic
print(timer())
print(now())
with freeze_time(timedelta(days=4), tick=True):
    print(timer())
    print(now())
    print(timer())

and get

4826002.177211833
2024-07-16 15:54:07.326647
1721454847.342787
2024-07-20 05:54:07.342823
1721454847.342848

as expected (the main change is that TTLCache timer goes four days into the future, causing the ttl cache to expire, so the second call to now() is correct) - before adding the timer function, I got the same result as the original comment - the result being the same value