Skip to content

Commit

Permalink
Add selective cache option
Browse files Browse the repository at this point in the history
This commit customizes the cache decorator so it is possible
to specify what parameters will be used as cache keys ~ i.e.
for function that we actually care about selected parameters that
makes the actual difference we can consider only specified ones
(by default all function parameters are considered).

Additionally it is possible to produce a human-readable string
instead of a calculated hash – this would make the generated
cache file browsable for us in case we need that...
  • Loading branch information
sdatko committed Jan 24, 2024
1 parent 2e4e2bf commit 00ffe8c
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 13 deletions.
4 changes: 2 additions & 2 deletions znoyder/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def fetch_templates_directory():
return templates_directory


@cache
@cache(readable=True)
def fetch_osp_projects(branch: str, filters: dict) -> list:
projects = {package.get('osp-project'): package.get('upstream')
for package in browser.get_packages(**filters)
Expand All @@ -104,7 +104,7 @@ def fetch_osp_projects(branch: str, filters: dict) -> list:
return projects


@cache
@cache('path', readable=True)
def discover_upstream_jobs(path, templates, pipelines):
return finder.find_jobs(path, templates, pipelines)

Expand Down
37 changes: 26 additions & 11 deletions znoyder/lib/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import yaml

from znoyder.lib import logger
from znoyder.lib.utils import get_args_dict
from znoyder.lib.yaml import NoAliasDumper


Expand All @@ -41,29 +42,43 @@ def __init__(self, filename=None):

self.reload()

def __call__(self, arg=None):
def __call__(self, *keys, readable=False):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
key = function.__qualname__ + '(' + hashlib.sha256(
pickle.dumps(
(args, kwargs)
)
).hexdigest() + ')'
call_args = get_args_dict(function, args, kwargs)
args_hash = ''

if key in self._cache:
return self._cache[key]
if call_args:
if keys: # filter out everything but wanted keys
for key in call_args.copy().keys():
if key not in keys:
del call_args[key]

if readable:
args_hash = str(list(call_args.values()))[1:-1]

else:
args_hash = hashlib.sha256(
pickle.dumps(call_args)
).hexdigest()

uuid = f'{function.__qualname__}({args_hash})'

if uuid in self._cache:
return self._cache[uuid]

else:
result = function(*args, **kwargs)
self._cache[key] = result
self._cache[uuid] = result
self.changed = True
return result

return wrapper

if callable(arg):
return decorator(arg)
if len(keys) == 1 and callable(keys[0]):
function, keys = keys[0], keys[1:]
return decorator(function)
else:
return decorator

Expand Down
6 changes: 6 additions & 0 deletions znoyder/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ def get_config_paths(local_path: str) -> list:
return zuul_config_files


def get_args_dict(fn, args, kwargs):
# by https://stackoverflow.com/a/40363565
args_names = fn.__code__.co_varnames[:fn.__code__.co_argcount]
return {**dict(zip(args_names, args)), **kwargs}


def match(string: str, specifier: str) -> bool:
'''Function checks if a given string is matched by a given specifier.
Expand Down
31 changes: 31 additions & 0 deletions znoyder/tests/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
#

import inspect
from unittest import TestCase
from unittest.mock import mock_open
from unittest.mock import patch
Expand Down Expand Up @@ -70,6 +71,36 @@ def fibonacci(n: int) -> int:
self.assertEqual(call1, call2)
self.assertEqual(call1, call3)

@patch('znoyder.tests.test_cache._noop')
def test_call_selected_parameters_and_readable(self, mock_noop):
cache = FileCache()

@cache('n', readable=True)
def fibonacci(n: int, unused: bool = False) -> int:
'''Helper function to be decorated in tests.'''
_noop()
unused = not unused
if n < 2:
return n
return fibonacci(n - 1, unused) + fibonacci(n - 2, unused)

call1 = fibonacci(10, True)
call2 = fibonacci(10, False)
call3 = fibonacci(10)

self.assertEqual(mock_noop.call_count, 11)
self.assertEqual(len(cache), 11)
self.assertEqual(call1, call2)
self.assertEqual(call1, call3)

uuid = '.'.join([
self.__class__.__name__,
inspect.currentframe().f_code.co_name,
'<locals>',
'fibonacci(10)'
])
self.assertTrue(uuid in cache._cache)

@patch('znoyder.tests.test_cache._noop')
def test_clear(self, mock_noop):
cache = FileCache()
Expand Down

0 comments on commit 00ffe8c

Please sign in to comment.