Skip to content
This repository was archived by the owner on Apr 22, 2020. It is now read-only.

caching: added support for cache-hints via timecache.return_value #218

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions easypy/caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ def _apply_defaults(bound_arguments):


class _TimeCache(DecoratingDescriptor):

class CacheHint(BaseException):
# inheriting from BaseException so we don't get accidentally caught by `try/except Exception`

def __init__(self, value, **kwargs):
self.value = value
self.__dict__.update(kwargs)

def __getattr__(self, name):
# this help keeping the list of hint parameters 'DRY', in 'return_value'
if name.startswith("_"):
raise AttributeError(name)
return None

def __init__(self, func, **kwargs):
update_wrapper(self, func) # this needs to be first to avoid overriding attributes we set
super().__init__(func=func, cached=True)
Expand Down Expand Up @@ -267,19 +281,54 @@ def __call__(self, *args, **kwargs):
pass # nothing to fuss with, cache does not expire
elif result is self.NOT_FOUND:
pass # cache is empty
elif self.get_ts_func() - ts >= self.expiration:
elif self.get_ts_func() >= ts:
# cache expired
result = self.NOT_FOUND
del self.cache[key]

if result is self.NOT_FOUND:
if self.log_recalculation:
_logger.debug('time cache expired, calculating new value for %s', self.__name__)
result = self.func(*args, **kwargs)
self.cache[key] = result, self.get_ts_func()
try:
hint = self.func(*args, **kwargs)
except self.CacheHint as exc:
hint = exc
else:
if not isinstance(hint, self.CacheHint):
hint = self.CacheHint(hint)
if hint.clear_cache:
self.cache_clear()
result = hint.value
if not hint.skip_cache:
ts = self.get_ts_func() + (self.expiration if hint.expiration is None else hint.expiration)
self.cache[key] = result, ts
elif hint.expiration:
raise Exception("Can't both 'skip_cache' AND set 'expiration'")

return result

@classmethod
def return_value(cls, value, skip_cache=False, expiration=None, clear_cache=False):
"""
Provide hints to the caching mechanism:

:param value: The value to return
:param skip_cache: If True, skip caching this value
:param expiration: Set an alternate expiration for this value
:param clear_cache: If True, clears the cache before caching this value

This method raises a 'CacheHint' exception in order to mimic the behavior of `return`::

@timecache(5)
def fib(n):
if n <= 1:
fib.return_value(n, skip_cache=True)
skip_cache = (n % 10) != 0 # cache every 10
fib.return_value(fib(n-2) + fib(n-1), skip_cache=skip_cache)
print("This line will not be executed")
"""
raise cls.CacheHint(value, skip_cache=skip_cache, expiration=expiration, clear_cache=clear_cache)

def cache_clear(self):
with self.main_lock:
for key, lock in dict(self.keyed_locks).items():
Expand Down Expand Up @@ -311,6 +360,17 @@ def timecache(expiration=0, typed=False, get_ts_func=time.time, log_recalculatio
:type ignored_keywords: iterable, optional
:param key_func: The function to use in order to create the item key, defaults to functools._make_key
:type key_func: callable, optional

The decorated function may use the 'func.return_value(...)' to provide some hints to the caching mechanism::

@timecache(5)
def fib(n):
if n <= 1:
fib.return_value(n, skip_cache=True)
skip_cache = (n % 10) != 0 # cache every 10
fib.return_value(fib(n-2) + fib(n-1), skip_cache=skip_cache)
print("This line will not be executed")

"""

def deco(func):
Expand All @@ -326,9 +386,6 @@ def deco(func):
return deco


timecache.__doc__ = _TimeCache.__doc__


@parametrizeable_decorator
def locking_cache(func=None, typed=False, log_recalculation=False, ignored_keywords=False):
"""
Expand Down
31 changes: 29 additions & 2 deletions tests/test_caching.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import random
import weakref
import gc
from collections import Counter

import pytest

from easypy.bunch import Bunch
from easypy.caching import timecache, PersistentCache, cached_property, locking_cache
from easypy.units import DAY
from easypy.caching import timecache, PersistentCache, cached_property
from easypy.resilience import resilient

_logger = getLogger(__name__)
Expand Down Expand Up @@ -273,3 +273,30 @@ def foo():
raise ExceptionLeakedThroughResilient()

assert foo() == 'default'


def test_cache_hints():
ts = 0
counter = Counter()

@timecache(1, get_ts_func=lambda: ts)
def foo(a):
counter[a] += 1
foo.return_value(a, skip_cache=a == 0, expiration=a, clear_cache=a < 0)

assert list(range(5)) == list(map(foo, range(5)))
assert list(range(5)) == list(map(foo, range(5)))
assert counter.pop(0) == 2
assert set(counter.values()) == {1} # make sure we reached the cache on all but '0'
counter.clear()

ts = 1
assert list(range(5)) == list(map(foo, range(5)))
assert counter.pop(0) == 1
assert counter.pop(1) == 1
assert not counter

assert set(foo.cache.keys()) == {1, 2, 3, 4}

foo(-1)
assert set(foo.cache.keys()) == {-1}