From a0975630fd5ad29a3bb0973a776381ae1f0b6769 Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Thu, 15 Apr 2021 23:11:00 +0800 Subject: [PATCH 1/7] Implement query to determine if result is from cache or not. --- cachier/core.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cachier/core.py b/cachier/core.py index 1e106d47..114da7b9 100644 --- a/cachier/core.py +++ b/cachier/core.py @@ -136,6 +136,7 @@ def cachier( def _cachier_decorator(func): core.set_func(func) + __is_from_cache = False @wraps(func) def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 @@ -143,6 +144,8 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 ignore_cache = kwds.pop('ignore_cache', False) overwrite_cache = kwds.pop('overwrite_cache', False) verbose_cache = kwds.pop('verbose_cache', False) + nonlocal __is_from_cache + __is_from_cache = False _print = lambda x: None # skipcq: FLK-E731 # noqa: E731 if verbose_cache: _print = print @@ -154,6 +157,7 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 if entry is not None: # pylint: disable=R0101 _print('Entry found.') if entry.get('value', None) is not None: + __is_from_cache = True _print('Cached result found.') if stale_after: now = datetime.datetime.now() @@ -167,6 +171,7 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 try: return core.wait_on_entry_calc(key) except RecalculationNeeded: + __is_from_cache = False return _calc_entry( core, key, func, args, kwds ) @@ -185,6 +190,7 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 finally: core.mark_entry_not_calculated(key) return entry['value'] + __is_from_cache = False _print('Calling decorated function and waiting') return _calc_entry(core, key, func, args, kwds) _print('And it is fresh!') @@ -194,6 +200,7 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 try: return core.wait_on_entry_calc(key) except RecalculationNeeded: + __is_from_cache = False return _calc_entry(core, key, func, args, kwds) _print('No entry found. No current calc. Calling like a boss.') return _calc_entry(core, key, func, args, kwds) @@ -213,9 +220,15 @@ def cache_dpath(): except AttributeError: return None + def is_from_cache(): + """Returns True if the result from the latest call is from the cache, False if not.""" + nonlocal __is_from_cache + return __is_from_cache + func_wrapper.clear_cache = clear_cache func_wrapper.clear_being_calculated = clear_being_calculated func_wrapper.cache_dpath = cache_dpath + func_wrapper.is_from_cache = is_from_cache return func_wrapper return _cachier_decorator From 221a0504e8aadc25deba0f5354d6d0c4c4d0b5d8 Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Thu, 15 Apr 2021 23:12:25 +0800 Subject: [PATCH 2/7] Unit tests for is_from_cache --- tests/test_pickle_core.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_pickle_core.py b/tests/test_pickle_core.py index abf2591f..5c2bfe3c 100644 --- a/tests/test_pickle_core.py +++ b/tests/test_pickle_core.py @@ -65,12 +65,16 @@ def test_stale_after(): """Testing the stale_after functionality.""" _stale_after_seconds.clear_cache() val1 = _stale_after_seconds(1, 2) + assert not _stale_after_seconds.is_from_cache() val2 = _stale_after_seconds(1, 2) + assert _stale_after_seconds.is_from_cache() val3 = _stale_after_seconds(1, 3) + assert not _stale_after_seconds.is_from_cache() assert val1 == val2 assert val1 != val3 sleep(3) val4 = _stale_after_seconds(1, 2) + assert not _stale_after_seconds.is_from_cache() assert val4 != val1 _stale_after_seconds.clear_cache() @@ -85,15 +89,20 @@ def test_stale_after_next_time(): """Testing the stale_after with next_time functionality.""" _stale_after_next_time.clear_cache() val1 = _stale_after_next_time(1, 2) + assert not _stale_after_next_time.is_from_cache() val2 = _stale_after_next_time(1, 2) + assert _stale_after_next_time.is_from_cache() val3 = _stale_after_next_time(1, 3) + assert not _stale_after_next_time.is_from_cache() assert val1 == val2 assert val1 != val3 sleep(SECONDS_IN_DELTA + 1) val4 = _stale_after_next_time(1, 2) + assert _stale_after_next_time.is_from_cache() assert val4 == val1 sleep(0.5) val5 = _stale_after_next_time(1, 2) + assert _stale_after_next_time.is_from_cache() assert val5 != val1 _stale_after_next_time.clear_cache() @@ -116,18 +125,24 @@ def test_overwrite_cache(): int2 = _random_num() assert int2 == int1 int3 = _random_num(overwrite_cache=True) + assert not _random_num.is_from_cache() assert int3 != int1 int4 = _random_num() + assert _random_num.is_from_cache() assert int4 == int3 _random_num.clear_cache() _random_num_with_arg.clear_cache() int1 = _random_num_with_arg('a') + assert not _random_num_with_arg.is_from_cache() int2 = _random_num_with_arg('a') + assert _random_num_with_arg.is_from_cache() assert int2 == int1 int3 = _random_num_with_arg('a', overwrite_cache=True) + assert not _random_num_with_arg.is_from_cache() assert int3 != int1 int4 = _random_num_with_arg('a') + assert _random_num_with_arg.is_from_cache() assert int4 == int3 _random_num_with_arg.clear_cache() @@ -136,22 +151,30 @@ def test_ignore_cache(): """Tests that the ignore_cache feature works correctly.""" _random_num.clear_cache() int1 = _random_num() + assert not _random_num.is_from_cache() int2 = _random_num() + assert _random_num.is_from_cache() assert int2 == int1 int3 = _random_num(ignore_cache=True) + assert not _random_num.is_from_cache() assert int3 != int1 int4 = _random_num() + assert _random_num.is_from_cache() assert int4 != int3 assert int4 == int1 _random_num.clear_cache() _random_num_with_arg.clear_cache() int1 = _random_num_with_arg('a') + assert not _random_num_with_arg.is_from_cache() int2 = _random_num_with_arg('a') + assert _random_num_with_arg.is_from_cache() assert int2 == int1 int3 = _random_num_with_arg('a', ignore_cache=True) + assert not _random_num_with_arg.is_from_cache() assert int3 != int1 int4 = _random_num_with_arg('a') + assert _random_num_with_arg.is_from_cache() assert int4 != int3 assert int4 == int1 _random_num_with_arg.clear_cache() From a2b3d7fe328d01ddf2223572016a14b5943ab75e Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Thu, 15 Apr 2021 23:12:47 +0800 Subject: [PATCH 3/7] Documentation for is_from_cache --- README.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.rst b/README.rst index 18b0a4e0..5e0279a1 100644 --- a/README.rst +++ b/README.rst @@ -83,6 +83,24 @@ You can add a default, pickle-based, persistent cache to your function - meaning """Your function now has a persistent cache mapped by argument values!""" return {'arg1': arg1, 'arg2': arg2} +Did the result come from the cache? +----------------------------------- + +You can find out of the function returned a value from the cache or not, by +calling the ``is_from_cache()`` function: + +.. code-block:: python + + >>> foo(1, 2) + {'arg1': 1, 'arg2': 2} + >>> foo.is_from_cache() + False + >>> foo(1, 2) + {'arg1': 1, 'arg2': 2} + >>> foo.is_from_cache() + True + + You can get the fully qualified path to the directory of cache files used by ``cachier`` (``~/.cachier`` by default) by calling the ``cache_dpath()`` function: .. code-block:: python From 5b316bc5a4cbbbfc121280a438333478aaf455bf Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Fri, 16 Apr 2021 07:51:35 +0800 Subject: [PATCH 4/7] Fill in test gaps for is_from_cache --- tests/test_pickle_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_pickle_core.py b/tests/test_pickle_core.py index 5c2bfe3c..3ec1d0b3 100644 --- a/tests/test_pickle_core.py +++ b/tests/test_pickle_core.py @@ -122,7 +122,9 @@ def test_overwrite_cache(): """Tests that the overwrite feature works correctly.""" _random_num.clear_cache() int1 = _random_num() + assert not _random_num.is_from_cache() int2 = _random_num() + assert _random_num.is_from_cache() assert int2 == int1 int3 = _random_num(overwrite_cache=True) assert not _random_num.is_from_cache() From ef5dd2767ccaf41a04d7ebd01a5ae2089afd3629 Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Fri, 16 Apr 2021 17:42:53 +0800 Subject: [PATCH 5/7] Implement Info listener class --- cachier/__init__.py | 2 +- cachier/core.py | 41 ++++++++++++++++++++++++++++------------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/cachier/__init__.py b/cachier/__init__.py index 335f80e9..80b2de97 100644 --- a/cachier/__init__.py +++ b/cachier/__init__.py @@ -1,4 +1,4 @@ -from .core import cachier +from .core import cachier, Info from ._version import get_versions __version__ = get_versions()['version'] del get_versions diff --git a/cachier/core.py b/cachier/core.py index 114da7b9..8a265330 100644 --- a/cachier/core.py +++ b/cachier/core.py @@ -73,6 +73,23 @@ def _calc_entry(core, key, func, args, kwds): core.mark_entry_not_calculated(key) +class Info: + """Holds function call metadata + """ + def __init__(self): + self.__is_from_cache = False + + @property + def is_from_cache(self): + return self.__is_from_cache + @is_from_cache.setter + def is_from_cache(self, state): + if isinstance(state, bool): + self.__is_from_cache = state + else: + raise TypeError("is_from_cache must be of type bool.") + + def cachier( stale_after=None, next_time=False, @@ -136,7 +153,6 @@ def cachier( def _cachier_decorator(func): core.set_func(func) - __is_from_cache = False @wraps(func) def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 @@ -144,8 +160,9 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 ignore_cache = kwds.pop('ignore_cache', False) overwrite_cache = kwds.pop('overwrite_cache', False) verbose_cache = kwds.pop('verbose_cache', False) - nonlocal __is_from_cache - __is_from_cache = False + cachier_info = kwds.pop('cachier_info', None) + if cachier_info: + cachier_info.is_from_cache = False _print = lambda x: None # skipcq: FLK-E731 # noqa: E731 if verbose_cache: _print = print @@ -157,7 +174,8 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 if entry is not None: # pylint: disable=R0101 _print('Entry found.') if entry.get('value', None) is not None: - __is_from_cache = True + if cachier_info: + cachier_info.is_from_cache = True _print('Cached result found.') if stale_after: now = datetime.datetime.now() @@ -171,7 +189,8 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 try: return core.wait_on_entry_calc(key) except RecalculationNeeded: - __is_from_cache = False + if cachier_info: + cachier_info.is_from_cache = False return _calc_entry( core, key, func, args, kwds ) @@ -190,7 +209,8 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 finally: core.mark_entry_not_calculated(key) return entry['value'] - __is_from_cache = False + if cachier_info: + cachier_info.is_from_cache = False _print('Calling decorated function and waiting') return _calc_entry(core, key, func, args, kwds) _print('And it is fresh!') @@ -200,7 +220,8 @@ def func_wrapper(*args, **kwds): # pylint: disable=C0111,R0911 try: return core.wait_on_entry_calc(key) except RecalculationNeeded: - __is_from_cache = False + if cachier_info: + cachier_info.is_from_cache = False return _calc_entry(core, key, func, args, kwds) _print('No entry found. No current calc. Calling like a boss.') return _calc_entry(core, key, func, args, kwds) @@ -220,15 +241,9 @@ def cache_dpath(): except AttributeError: return None - def is_from_cache(): - """Returns True if the result from the latest call is from the cache, False if not.""" - nonlocal __is_from_cache - return __is_from_cache - func_wrapper.clear_cache = clear_cache func_wrapper.clear_being_calculated = clear_being_calculated func_wrapper.cache_dpath = cache_dpath - func_wrapper.is_from_cache = is_from_cache return func_wrapper return _cachier_decorator From dcf341638d84e4cbda7a12cd19c27cdd222c0d6a Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Fri, 16 Apr 2021 17:58:12 +0800 Subject: [PATCH 6/7] Tests for Info listener class --- tests/test_pickle_core.py | 63 +++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/tests/test_pickle_core.py b/tests/test_pickle_core.py index 3ec1d0b3..52669f62 100644 --- a/tests/test_pickle_core.py +++ b/tests/test_pickle_core.py @@ -27,7 +27,7 @@ import hashlib import pandas as pd -from cachier import cachier +from cachier import cachier, Info from cachier.pickle_core import DEF_CACHIER_DIR @@ -65,16 +65,12 @@ def test_stale_after(): """Testing the stale_after functionality.""" _stale_after_seconds.clear_cache() val1 = _stale_after_seconds(1, 2) - assert not _stale_after_seconds.is_from_cache() val2 = _stale_after_seconds(1, 2) - assert _stale_after_seconds.is_from_cache() val3 = _stale_after_seconds(1, 3) - assert not _stale_after_seconds.is_from_cache() assert val1 == val2 assert val1 != val3 sleep(3) val4 = _stale_after_seconds(1, 2) - assert not _stale_after_seconds.is_from_cache() assert val4 != val1 _stale_after_seconds.clear_cache() @@ -89,20 +85,15 @@ def test_stale_after_next_time(): """Testing the stale_after with next_time functionality.""" _stale_after_next_time.clear_cache() val1 = _stale_after_next_time(1, 2) - assert not _stale_after_next_time.is_from_cache() val2 = _stale_after_next_time(1, 2) - assert _stale_after_next_time.is_from_cache() val3 = _stale_after_next_time(1, 3) - assert not _stale_after_next_time.is_from_cache() assert val1 == val2 assert val1 != val3 sleep(SECONDS_IN_DELTA + 1) val4 = _stale_after_next_time(1, 2) - assert _stale_after_next_time.is_from_cache() assert val4 == val1 sleep(0.5) val5 = _stale_after_next_time(1, 2) - assert _stale_after_next_time.is_from_cache() assert val5 != val1 _stale_after_next_time.clear_cache() @@ -122,29 +113,21 @@ def test_overwrite_cache(): """Tests that the overwrite feature works correctly.""" _random_num.clear_cache() int1 = _random_num() - assert not _random_num.is_from_cache() int2 = _random_num() - assert _random_num.is_from_cache() assert int2 == int1 int3 = _random_num(overwrite_cache=True) - assert not _random_num.is_from_cache() assert int3 != int1 int4 = _random_num() - assert _random_num.is_from_cache() assert int4 == int3 _random_num.clear_cache() _random_num_with_arg.clear_cache() int1 = _random_num_with_arg('a') - assert not _random_num_with_arg.is_from_cache() int2 = _random_num_with_arg('a') - assert _random_num_with_arg.is_from_cache() assert int2 == int1 int3 = _random_num_with_arg('a', overwrite_cache=True) - assert not _random_num_with_arg.is_from_cache() assert int3 != int1 int4 = _random_num_with_arg('a') - assert _random_num_with_arg.is_from_cache() assert int4 == int3 _random_num_with_arg.clear_cache() @@ -153,35 +136,63 @@ def test_ignore_cache(): """Tests that the ignore_cache feature works correctly.""" _random_num.clear_cache() int1 = _random_num() - assert not _random_num.is_from_cache() int2 = _random_num() - assert _random_num.is_from_cache() assert int2 == int1 int3 = _random_num(ignore_cache=True) - assert not _random_num.is_from_cache() assert int3 != int1 int4 = _random_num() - assert _random_num.is_from_cache() assert int4 != int3 assert int4 == int1 _random_num.clear_cache() _random_num_with_arg.clear_cache() int1 = _random_num_with_arg('a') - assert not _random_num_with_arg.is_from_cache() int2 = _random_num_with_arg('a') - assert _random_num_with_arg.is_from_cache() assert int2 == int1 int3 = _random_num_with_arg('a', ignore_cache=True) - assert not _random_num_with_arg.is_from_cache() assert int3 != int1 int4 = _random_num_with_arg('a') - assert _random_num_with_arg.is_from_cache() assert int4 != int3 assert int4 == int1 _random_num_with_arg.clear_cache() +def test_cachier_info(): + """Tests that the cachier_info feature works correctly.""" + _random_num.clear_cache() + call_info = Info() + int1 = _random_num(cachier_info=call_info) + assert not call_info.is_from_cache + int2 = _random_num(cachier_info=call_info) + assert call_info.is_from_cache + assert int2 == int1 + int3 = _random_num(ignore_cache=True, cachier_info=call_info) + assert not call_info.is_from_cache + assert int3 != int1 + int4 = _random_num(cachier_info=call_info) + assert call_info.is_from_cache + assert int4 != int3 + assert int4 == int1 + _random_num.clear_cache() + + _random_num_with_arg.clear_cache() + int1 = _random_num_with_arg('a', cachier_info=call_info) + assert not call_info.is_from_cache + int2 = _random_num_with_arg('a', cachier_info=call_info) + assert call_info.is_from_cache + assert int2 == int1 + int3 = _random_num_with_arg('a', ignore_cache=True, cachier_info=call_info) + assert not call_info.is_from_cache + assert int3 != int1 + int4 = _random_num_with_arg('a', cachier_info=call_info) + assert call_info.is_from_cache + assert int4 != int3 + assert int4 == int1 + _random_num_with_arg.clear_cache() + pass + + + @cachier() def _takes_time(arg_1, arg_2): """Some function.""" From db7e220b4efab1f87869aabefad49a79a29c7aa7 Mon Sep 17 00:00:00 2001 From: Kit Monisit Date: Fri, 16 Apr 2021 17:58:38 +0800 Subject: [PATCH 7/7] Documentation for Info listener class --- README.rst | 14 ++++++++------ tests/test_pickle_core.py | 2 -- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 5e0279a1..b836dfe7 100644 --- a/README.rst +++ b/README.rst @@ -86,18 +86,20 @@ You can add a default, pickle-based, persistent cache to your function - meaning Did the result come from the cache? ----------------------------------- -You can find out of the function returned a value from the cache or not, by -calling the ``is_from_cache()`` function: +You can find out of the function returned a value from the cache or not, passing +an ``Info`` object to the decorated function, then inspecting its +``is_from_cache`` attribute: .. code-block:: python - >>> foo(1, 2) + >>> call_info = cachier.Info() + >>> foo(1, 2, cachier_info=call_info) {'arg1': 1, 'arg2': 2} - >>> foo.is_from_cache() + >>> call_info.is_from_cache False - >>> foo(1, 2) + >>> foo(1, 2, cachier_info=call_info) {'arg1': 1, 'arg2': 2} - >>> foo.is_from_cache() + >>> call_info.is_from_cache True diff --git a/tests/test_pickle_core.py b/tests/test_pickle_core.py index 52669f62..ccb09755 100644 --- a/tests/test_pickle_core.py +++ b/tests/test_pickle_core.py @@ -189,8 +189,6 @@ def test_cachier_info(): assert int4 != int3 assert int4 == int1 _random_num_with_arg.clear_cache() - pass - @cachier()