Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement "in memory" backend #39

Merged
merged 8 commits into from
Nov 17, 2020
Merged
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
32 changes: 29 additions & 3 deletions cachier/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from .pickle_core import _PickleCore
from .mongo_core import _MongoCore, RecalculationNeeded
from .memory_core import _MemoryCore


MAX_WORKERS_ENVAR_NAME = 'CACHIER_MAX_WORKERS'
Expand Down Expand Up @@ -73,10 +74,15 @@ def _calc_entry(core, key, func, args, kwds):
core.mark_entry_not_calculated(key)


class MissingMongetter(ValueError):
"""Thrown when the mongetter keyword argument is missing."""


def cachier(
stale_after=None,
next_time=False,
pickle_reload=True,
backend=None,
mongetter=None,
cache_dir=None,
hash_params=None,
Expand Down Expand Up @@ -110,6 +116,10 @@ def cachier(
A callable that takes no arguments and returns a pymongo.Collection
object with writing permissions. If unset a local pickle cache is used
instead.
backend : str, optional
The name of the backend to use. If None, defaults to 'mongo' when
the ``mongetter`` argument is passed, otherwise defaults to 'pickle'.
Valid options currently include 'pickle' and 'mongo'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add 'memory' as a valid option.

cache_dir : str, optional
A fully qualified path to a file directory to be used for cache files.
The running process must have running permissions to this folder. If
Expand All @@ -131,15 +141,31 @@ def cachier(
# print('stale_after={}'.format(stale_after))
# print('next_time={}'.format(next_time))

if mongetter:
core = _MongoCore(mongetter, stale_after, next_time, wait_for_calc_timeout)
else:
# The default is calculated dynamically to maintain previous behavior
# to default to pickle unless the ``mongetter`` argument is given.
if backend is None:
backend = 'pickle' if mongetter is None else 'mongo'

if backend == 'pickle':
core = _PickleCore( # pylint: disable=R0204
stale_after=stale_after,
next_time=next_time,
reload=pickle_reload,
cache_dir=cache_dir,
)
elif backend == 'mongo':
if mongetter is None:
raise MissingMongetter('must specify ``mongetter`` when using the mongo core')
core = _MongoCore(mongetter, stale_after, next_time, wait_for_calc_timeout)
elif backend == 'memory':
core = _MemoryCore(stale_after=stale_after, next_time=next_time)
elif backend == 'redis':
raise NotImplementedError(
'A Redis backend has not yet been implemented. '
'Please see https://github.com/shaypal5/cachier/issues/4'
)
else:
raise ValueError('specified an invalid core: {}'.format(backend))

def _cachier_decorator(func):
core.set_func(func)
Expand Down
67 changes: 67 additions & 0 deletions cachier/memory_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""A memory-based caching core for cachier."""

from collections import defaultdict
from datetime import datetime

from .base_core import _BaseCore


class _MemoryCore(_BaseCore):
"""The pickle core class for cachier.

Parameters
----------
stale_after : datetime.timedelta, optional
See :class:`_BaseCore` documentation.
next_time : bool, optional
See :class:`_BaseCore` documentation.
"""

def __init__(self, stale_after, next_time):
super().__init__(stale_after=stale_after, next_time=next_time)
self.cache = {}

def get_entry_by_key(self, key, reload=False): # pylint: disable=W0221
return key, self.cache.get(key, None)

def get_entry(self, args, kwds, hash_params):
key = args + tuple(sorted(kwds.items())) if hash_params is None else hash_params(args, kwds)
return self.get_entry_by_key(key)

def set_entry(self, key, func_res):
self.cache[key] = {
'value': func_res,
'time': datetime.now(),
'stale': False,
'being_calculated': False,
}

def mark_entry_being_calculated(self, key):
try:
self.cache[key]['being_calculated'] = True
except KeyError:
self.cache[key] = {
'value': None,
'time': datetime.now(),
'stale': False,
'being_calculated': True,
}

def mark_entry_not_calculated(self, key):
try:
self.cache[key]['being_calculated'] = False
except KeyError:
pass # that's ok, we don't need an entry in that case

def wait_on_entry_calc(self, key):
entry = self.cache[key]
# I don't think waiting is necessary for this one
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it turns out this assumption isn't at all correct 😆

# if not entry['being_calculated']:
return entry['value']

def clear_cache(self):
self.cache.clear()

def clear_being_calculated(self):
for value in self.cache.values():
value['being_calculated'] = False
29 changes: 29 additions & 0 deletions tests/test_core_lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Testing the MongoDB core of cachier."""

from cachier import cachier
from cachier.core import MissingMongetter


def test_bad_name():
"""Test that the appropriate exception is thrown when an invalid backend is given."""
name = 'nope'
try:
@cachier(backend=name)
def func():
pass
except ValueError as e:
assert name in e.args[0]
else:
assert False


def test_missing_mongetter():
"""Test that the appropriate exception is thrown when forgetting to specify the mongetter."""
try:
@cachier(backend='mongo', mongetter=None)
def func():
pass
except MissingMongetter:
assert True
else:
assert False
Loading