Update diskcache 5.1.0 (40ce0de) → 5.4.0 (1cb1425).

This commit is contained in:
JackDandy 2023-01-13 02:13:40 +00:00
parent 767ae2962f
commit 3a10574881
8 changed files with 503 additions and 511 deletions

View file

@ -4,6 +4,7 @@
* Add Filelock 3.9.0 (ce3e891) * Add Filelock 3.9.0 (ce3e891)
* Remove Lockfile no longer used by Cachecontrol * Remove Lockfile no longer used by Cachecontrol
* Update Msgpack 1.0.0 (fa7d744) to 1.0.4 (b5acfd5) * Update Msgpack 1.0.0 (fa7d744) to 1.0.4 (b5acfd5)
* Update diskcache 5.1.0 (40ce0de) to 5.4.0 (1cb1425)
* Update Rarfile 4.0 (55fe778) to 4.1a1 (8a72967) * Update Rarfile 4.0 (55fe778) to 4.1a1 (8a72967)
* Update UnRar x64 for Windows 6.11 to 6.20 * Update UnRar x64 for Windows 6.11 to 6.20
* Update Send2Trash 1.5.0 (66afce7) to 1.8.1b0 (0ef9b32) * Update Send2Trash 1.5.0 (66afce7) to 1.8.1b0 (0ef9b32)

View file

@ -3,17 +3,31 @@ DiskCache API Reference
======================= =======================
The :doc:`tutorial` provides a helpful walkthrough of most methods. The :doc:`tutorial` provides a helpful walkthrough of most methods.
""" """
from .core import ( from .core import (
Cache, Disk, EmptyDirWarning, JSONDisk, UnknownFileWarning, Timeout DEFAULT_SETTINGS,
ENOVAL,
EVICTION_POLICY,
UNKNOWN,
Cache,
Disk,
EmptyDirWarning,
JSONDisk,
Timeout,
UnknownFileWarning,
) )
from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN
from .fanout import FanoutCache from .fanout import FanoutCache
from .persistent import Deque, Index from .persistent import Deque, Index
from .recipes import Averager, BoundedSemaphore, Lock, RLock from .recipes import (
from .recipes import barrier, memoize_stampede, throttle Averager,
BoundedSemaphore,
Lock,
RLock,
barrier,
memoize_stampede,
throttle,
)
__all__ = [ __all__ = [
'Averager', 'Averager',
@ -40,14 +54,15 @@ __all__ = [
try: try:
from .djangocache import DjangoCache # noqa from .djangocache import DjangoCache # noqa
__all__.append('DjangoCache') __all__.append('DjangoCache')
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except # pragma: no cover
# Django not installed or not setup so ignore. # Django not installed or not setup so ignore.
pass pass
__title__ = 'diskcache' __title__ = 'diskcache'
__version__ = '5.1.0' __version__ = '5.4.0'
__build__ = 0x050100 __build__ = 0x050400
__author__ = 'Grant Jenks' __author__ = 'Grant Jenks'
__license__ = 'Apache 2.0' __license__ = 'Apache 2.0'
__copyright__ = 'Copyright 2016-2020 Grant Jenks' __copyright__ = 'Copyright 2016-2022 Grant Jenks'

View file

@ -1 +1 @@
"Command line interface to disk cache." """Command line interface to disk cache."""

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,12 @@
"Django-compatible disk and file backed cache." """Django-compatible disk and file backed cache."""
from functools import wraps from functools import wraps
from django.core.cache.backends.base import BaseCache from django.core.cache.backends.base import BaseCache
try: try:
from django.core.cache.backends.base import DEFAULT_TIMEOUT from django.core.cache.backends.base import DEFAULT_TIMEOUT
except ImportError: except ImportError: # pragma: no cover
# For older versions of Django simply use 300 seconds. # For older versions of Django simply use 300 seconds.
DEFAULT_TIMEOUT = 300 DEFAULT_TIMEOUT = 300
@ -14,7 +15,8 @@ from .fanout import FanoutCache
class DjangoCache(BaseCache): class DjangoCache(BaseCache):
"Django-compatible disk and file backed cache." """Django-compatible disk and file backed cache."""
def __init__(self, directory, params): def __init__(self, directory, params):
"""Initialize DjangoCache instance. """Initialize DjangoCache instance.
@ -28,13 +30,11 @@ class DjangoCache(BaseCache):
options = params.get('OPTIONS', {}) options = params.get('OPTIONS', {})
self._cache = FanoutCache(directory, shards, timeout, **options) self._cache = FanoutCache(directory, shards, timeout, **options)
@property @property
def directory(self): def directory(self):
"""Cache directory.""" """Cache directory."""
return self._cache.directory return self._cache.directory
def cache(self, name): def cache(self, name):
"""Return Cache with given `name` in subdirectory. """Return Cache with given `name` in subdirectory.
@ -44,7 +44,6 @@ class DjangoCache(BaseCache):
""" """
return self._cache.cache(name) return self._cache.cache(name)
def deque(self, name): def deque(self, name):
"""Return Deque with given `name` in subdirectory. """Return Deque with given `name` in subdirectory.
@ -54,7 +53,6 @@ class DjangoCache(BaseCache):
""" """
return self._cache.deque(name) return self._cache.deque(name)
def index(self, name): def index(self, name):
"""Return Index with given `name` in subdirectory. """Return Index with given `name` in subdirectory.
@ -64,9 +62,16 @@ class DjangoCache(BaseCache):
""" """
return self._cache.index(name) return self._cache.index(name)
def add(
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, self,
read=False, tag=None, retry=True): key,
value,
timeout=DEFAULT_TIMEOUT,
version=None,
read=False,
tag=None,
retry=True,
):
"""Set a value in the cache if the key does not already exist. If """Set a value in the cache if the key does not already exist. If
timeout is given, that timeout will be used for the key; otherwise the timeout is given, that timeout will be used for the key; otherwise the
default cache timeout will be used. default cache timeout will be used.
@ -89,9 +94,16 @@ class DjangoCache(BaseCache):
timeout = self.get_backend_timeout(timeout=timeout) timeout = self.get_backend_timeout(timeout=timeout)
return self._cache.add(key, value, timeout, read, tag, retry) return self._cache.add(key, value, timeout, read, tag, retry)
def get(
def get(self, key, default=None, version=None, read=False, self,
expire_time=False, tag=False, retry=False): key,
default=None,
version=None,
read=False,
expire_time=False,
tag=False,
retry=False,
):
"""Fetch a given key from the cache. If the key does not exist, return """Fetch a given key from the cache. If the key does not exist, return
default, which itself defaults to None. default, which itself defaults to None.
@ -111,7 +123,6 @@ class DjangoCache(BaseCache):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
return self._cache.get(key, default, read, expire_time, tag, retry) return self._cache.get(key, default, read, expire_time, tag, retry)
def read(self, key, version=None): def read(self, key, version=None):
"""Return file handle corresponding to `key` from Cache. """Return file handle corresponding to `key` from Cache.
@ -124,9 +135,16 @@ class DjangoCache(BaseCache):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
return self._cache.read(key) return self._cache.read(key)
def set(
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, self,
read=False, tag=None, retry=True): key,
value,
timeout=DEFAULT_TIMEOUT,
version=None,
read=False,
tag=None,
retry=True,
):
"""Set a value in the cache. If timeout is given, that timeout will be """Set a value in the cache. If timeout is given, that timeout will be
used for the key; otherwise the default cache timeout will be used. used for the key; otherwise the default cache timeout will be used.
@ -146,7 +164,6 @@ class DjangoCache(BaseCache):
timeout = self.get_backend_timeout(timeout=timeout) timeout = self.get_backend_timeout(timeout=timeout)
return self._cache.set(key, value, timeout, read, tag, retry) return self._cache.set(key, value, timeout, read, tag, retry)
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None, retry=True): def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None, retry=True):
"""Touch a key in the cache. If timeout is given, that timeout will be """Touch a key in the cache. If timeout is given, that timeout will be
used for the key; otherwise the default cache timeout will be used. used for the key; otherwise the default cache timeout will be used.
@ -164,9 +181,15 @@ class DjangoCache(BaseCache):
timeout = self.get_backend_timeout(timeout=timeout) timeout = self.get_backend_timeout(timeout=timeout)
return self._cache.touch(key, timeout, retry) return self._cache.touch(key, timeout, retry)
def pop(
def pop(self, key, default=None, version=None, expire_time=False, self,
tag=False, retry=True): key,
default=None,
version=None,
expire_time=False,
tag=False,
retry=True,
):
"""Remove corresponding item for `key` from cache and return value. """Remove corresponding item for `key` from cache and return value.
If `key` is missing, return `default`. If `key` is missing, return `default`.
@ -186,7 +209,6 @@ class DjangoCache(BaseCache):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
return self._cache.pop(key, default, expire_time, tag, retry) return self._cache.pop(key, default, expire_time, tag, retry)
def delete(self, key, version=None, retry=True): def delete(self, key, version=None, retry=True):
"""Delete a key from the cache, failing silently. """Delete a key from the cache, failing silently.
@ -198,8 +220,7 @@ class DjangoCache(BaseCache):
""" """
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
self._cache.delete(key, retry) return self._cache.delete(key, retry)
def incr(self, key, delta=1, version=None, default=None, retry=True): def incr(self, key, delta=1, version=None, default=None, retry=True):
"""Increment value by delta for item with key. """Increment value by delta for item with key.
@ -230,7 +251,6 @@ class DjangoCache(BaseCache):
except KeyError: except KeyError:
raise ValueError("Key '%s' not found" % key) from None raise ValueError("Key '%s' not found" % key) from None
def decr(self, key, delta=1, version=None, default=None, retry=True): def decr(self, key, delta=1, version=None, default=None, retry=True):
"""Decrement value by delta for item with key. """Decrement value by delta for item with key.
@ -259,7 +279,6 @@ class DjangoCache(BaseCache):
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
return self.incr(key, -delta, version, default, retry) return self.incr(key, -delta, version, default, retry)
def has_key(self, key, version=None): def has_key(self, key, version=None):
"""Returns True if the key is in the cache and has not expired. """Returns True if the key is in the cache and has not expired.
@ -271,7 +290,6 @@ class DjangoCache(BaseCache):
key = self.make_key(key, version=version) key = self.make_key(key, version=version)
return key in self._cache return key in self._cache
def expire(self): def expire(self):
"""Remove expired items from cache. """Remove expired items from cache.
@ -280,7 +298,6 @@ class DjangoCache(BaseCache):
""" """
return self._cache.expire() return self._cache.expire()
def stats(self, enable=True, reset=False): def stats(self, enable=True, reset=False):
"""Return cache statistics hits and misses. """Return cache statistics hits and misses.
@ -291,7 +308,6 @@ class DjangoCache(BaseCache):
""" """
return self._cache.stats(enable=enable, reset=reset) return self._cache.stats(enable=enable, reset=reset)
def create_tag_index(self): def create_tag_index(self):
"""Create tag index on cache database. """Create tag index on cache database.
@ -302,7 +318,6 @@ class DjangoCache(BaseCache):
""" """
self._cache.create_tag_index() self._cache.create_tag_index()
def drop_tag_index(self): def drop_tag_index(self):
"""Drop tag index on cache database. """Drop tag index on cache database.
@ -311,7 +326,6 @@ class DjangoCache(BaseCache):
""" """
self._cache.drop_tag_index() self._cache.drop_tag_index()
def evict(self, tag): def evict(self, tag):
"""Remove items with matching `tag` from cache. """Remove items with matching `tag` from cache.
@ -321,7 +335,6 @@ class DjangoCache(BaseCache):
""" """
return self._cache.evict(tag) return self._cache.evict(tag)
def cull(self): def cull(self):
"""Cull items from cache until volume is less than size limit. """Cull items from cache until volume is less than size limit.
@ -330,18 +343,15 @@ class DjangoCache(BaseCache):
""" """
return self._cache.cull() return self._cache.cull()
def clear(self): def clear(self):
"Remove *all* values from the cache at once." """Remove *all* values from the cache at once."""
return self._cache.clear() return self._cache.clear()
def close(self, **kwargs): def close(self, **kwargs):
"Close the cache connection." """Close the cache connection."""
# pylint: disable=unused-argument # pylint: disable=unused-argument
self._cache.close() self._cache.close()
def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT):
"""Return seconds to expiration. """Return seconds to expiration.
@ -356,9 +366,15 @@ class DjangoCache(BaseCache):
timeout = -1 timeout = -1
return None if timeout is None else timeout return None if timeout is None else timeout
def memoize(
def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None, self,
typed=False, tag=None): name=None,
timeout=DEFAULT_TIMEOUT,
version=None,
typed=False,
tag=None,
ignore=(),
):
"""Memoizing cache decorator. """Memoizing cache decorator.
Decorator to wrap callable with memoizing function using cache. Decorator to wrap callable with memoizing function using cache.
@ -392,6 +408,7 @@ class DjangoCache(BaseCache):
:param int version: key version number (default None, cache parameter) :param int version: key version number (default None, cache parameter)
:param bool typed: cache different types separately (default False) :param bool typed: cache different types separately (default False)
:param str tag: text to associate with arguments (default None) :param str tag: text to associate with arguments (default None)
:param set ignore: positional or keyword args to ignore (default ())
:return: callable decorator :return: callable decorator
""" """
@ -400,12 +417,12 @@ class DjangoCache(BaseCache):
raise TypeError('name cannot be callable') raise TypeError('name cannot be callable')
def decorator(func): def decorator(func):
"Decorator created by memoize() for callable `func`." """Decorator created by memoize() for callable `func`."""
base = (full_name(func),) if name is None else (name,) base = (full_name(func),) if name is None else (name,)
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
"Wrapper for callable to cache arguments and return values." """Wrapper for callable to cache arguments and return values."""
key = wrapper.__cache_key__(*args, **kwargs) key = wrapper.__cache_key__(*args, **kwargs)
result = self.get(key, ENOVAL, version, retry=True) result = self.get(key, ENOVAL, version, retry=True)
@ -418,14 +435,19 @@ class DjangoCache(BaseCache):
) )
if valid_timeout: if valid_timeout:
self.set( self.set(
key, result, timeout, version, tag=tag, retry=True, key,
result,
timeout,
version,
tag=tag,
retry=True,
) )
return result return result
def __cache_key__(*args, **kwargs): def __cache_key__(*args, **kwargs):
"Make key for cache given function arguments." """Make key for cache given function arguments."""
return args_to_key(base, args, kwargs, typed) return args_to_key(base, args, kwargs, typed, ignore)
wrapper.__cache_key__ = __cache_key__ wrapper.__cache_key__ = __cache_key__
return wrapper return wrapper

View file

@ -1,4 +1,4 @@
"Fanout cache automatically shards keys and values." """Fanout cache automatically shards keys and values."""
import contextlib as cl import contextlib as cl
import functools import functools
@ -9,14 +9,16 @@ import sqlite3
import tempfile import tempfile
import time import time
from .core import ENOVAL, DEFAULT_SETTINGS, Cache, Disk, Timeout from .core import DEFAULT_SETTINGS, ENOVAL, Cache, Disk, Timeout
from .persistent import Deque, Index from .persistent import Deque, Index
class FanoutCache(object): class FanoutCache:
"Cache that shards keys and values." """Cache that shards keys and values."""
def __init__(self, directory=None, shards=8, timeout=0.010, disk=Disk,
**settings): def __init__(
self, directory=None, shards=8, timeout=0.010, disk=Disk, **settings
):
"""Initialize cache instance. """Initialize cache instance.
:param str directory: cache directory :param str directory: cache directory
@ -36,6 +38,7 @@ class FanoutCache(object):
self._count = shards self._count = shards
self._directory = directory self._directory = directory
self._disk = disk
self._shards = tuple( self._shards = tuple(
Cache( Cache(
directory=op.join(directory, '%03d' % num), directory=op.join(directory, '%03d' % num),
@ -51,20 +54,17 @@ class FanoutCache(object):
self._deques = {} self._deques = {}
self._indexes = {} self._indexes = {}
@property @property
def directory(self): def directory(self):
"""Cache directory.""" """Cache directory."""
return self._directory return self._directory
def __getattr__(self, name): def __getattr__(self, name):
safe_names = {'timeout', 'disk'} safe_names = {'timeout', 'disk'}
valid_name = name in DEFAULT_SETTINGS or name in safe_names valid_name = name in DEFAULT_SETTINGS or name in safe_names
assert valid_name, 'cannot access {} in cache shard'.format(name) assert valid_name, 'cannot access {} in cache shard'.format(name)
return getattr(self._shards[0], name) return getattr(self._shards[0], name)
@cl.contextmanager @cl.contextmanager
def transact(self, retry=True): def transact(self, retry=True):
"""Context manager to perform a transaction by locking the cache. """Context manager to perform a transaction by locking the cache.
@ -98,7 +98,6 @@ class FanoutCache(object):
stack.enter_context(shard_transaction) stack.enter_context(shard_transaction)
yield yield
def set(self, key, value, expire=None, read=False, tag=None, retry=False): def set(self, key, value, expire=None, read=False, tag=None, retry=False):
"""Set `key` and `value` item in cache. """Set `key` and `value` item in cache.
@ -125,7 +124,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return False return False
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""Set `key` and `value` item in cache. """Set `key` and `value` item in cache.
@ -139,7 +137,6 @@ class FanoutCache(object):
shard = self._shards[index] shard = self._shards[index]
shard[key] = value shard[key] = value
def touch(self, key, expire=None, retry=False): def touch(self, key, expire=None, retry=False):
"""Touch `key` in cache and update `expire` time. """Touch `key` in cache and update `expire` time.
@ -160,7 +157,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return False return False
def add(self, key, value, expire=None, read=False, tag=None, retry=False): def add(self, key, value, expire=None, read=False, tag=None, retry=False):
"""Add `key` and `value` item to cache. """Add `key` and `value` item to cache.
@ -192,7 +188,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return False return False
def incr(self, key, delta=1, default=0, retry=False): def incr(self, key, delta=1, default=0, retry=False):
"""Increment value by delta for item with key. """Increment value by delta for item with key.
@ -224,7 +219,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return None return None
def decr(self, key, delta=1, default=0, retry=False): def decr(self, key, delta=1, default=0, retry=False):
"""Decrement value by delta for item with key. """Decrement value by delta for item with key.
@ -259,9 +253,15 @@ class FanoutCache(object):
except Timeout: except Timeout:
return None return None
def get(
def get(self, key, default=None, read=False, expire_time=False, tag=False, self,
retry=False): key,
default=None,
read=False,
expire_time=False,
tag=False,
retry=False,
):
"""Retrieve value from cache. If `key` is missing, return `default`. """Retrieve value from cache. If `key` is missing, return `default`.
If database timeout occurs then returns `default` unless `retry` is set If database timeout occurs then returns `default` unless `retry` is set
@ -285,7 +285,6 @@ class FanoutCache(object):
except (Timeout, sqlite3.OperationalError): except (Timeout, sqlite3.OperationalError):
return default return default
def __getitem__(self, key): def __getitem__(self, key):
"""Return corresponding value for `key` from cache. """Return corresponding value for `key` from cache.
@ -300,7 +299,6 @@ class FanoutCache(object):
shard = self._shards[index] shard = self._shards[index]
return shard[key] return shard[key]
def read(self, key): def read(self, key):
"""Return file handle corresponding to `key` from cache. """Return file handle corresponding to `key` from cache.
@ -314,7 +312,6 @@ class FanoutCache(object):
raise KeyError(key) raise KeyError(key)
return handle return handle
def __contains__(self, key): def __contains__(self, key):
"""Return `True` if `key` matching item is found in cache. """Return `True` if `key` matching item is found in cache.
@ -326,8 +323,9 @@ class FanoutCache(object):
shard = self._shards[index] shard = self._shards[index]
return key in shard return key in shard
def pop(
def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # noqa: E501 self, key, default=None, expire_time=False, tag=False, retry=False
): # noqa: E501
"""Remove corresponding item for `key` from cache and return value. """Remove corresponding item for `key` from cache and return value.
If `key` is missing, return `default`. If `key` is missing, return `default`.
@ -353,7 +351,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return default return default
def delete(self, key, retry=False): def delete(self, key, retry=False):
"""Delete corresponding item for `key` from cache. """Delete corresponding item for `key` from cache.
@ -374,7 +371,6 @@ class FanoutCache(object):
except Timeout: except Timeout:
return False return False
def __delitem__(self, key): def __delitem__(self, key):
"""Delete corresponding item for `key` from cache. """Delete corresponding item for `key` from cache.
@ -388,7 +384,6 @@ class FanoutCache(object):
shard = self._shards[index] shard = self._shards[index]
del shard[key] del shard[key]
def check(self, fix=False, retry=False): def check(self, fix=False, retry=False):
"""Check database and file system consistency. """Check database and file system consistency.
@ -412,7 +407,6 @@ class FanoutCache(object):
warnings = (shard.check(fix, retry) for shard in self._shards) warnings = (shard.check(fix, retry) for shard in self._shards)
return functools.reduce(operator.iadd, warnings, []) return functools.reduce(operator.iadd, warnings, [])
def expire(self, retry=False): def expire(self, retry=False):
"""Remove expired items from cache. """Remove expired items from cache.
@ -425,7 +419,6 @@ class FanoutCache(object):
""" """
return self._remove('expire', args=(time.time(),), retry=retry) return self._remove('expire', args=(time.time(),), retry=retry)
def create_tag_index(self): def create_tag_index(self):
"""Create tag index on cache database. """Create tag index on cache database.
@ -437,7 +430,6 @@ class FanoutCache(object):
for shard in self._shards: for shard in self._shards:
shard.create_tag_index() shard.create_tag_index()
def drop_tag_index(self): def drop_tag_index(self):
"""Drop tag index on cache database. """Drop tag index on cache database.
@ -447,7 +439,6 @@ class FanoutCache(object):
for shard in self._shards: for shard in self._shards:
shard.drop_tag_index() shard.drop_tag_index()
def evict(self, tag, retry=False): def evict(self, tag, retry=False):
"""Remove items with matching `tag` from cache. """Remove items with matching `tag` from cache.
@ -461,7 +452,6 @@ class FanoutCache(object):
""" """
return self._remove('evict', args=(tag,), retry=retry) return self._remove('evict', args=(tag,), retry=retry)
def cull(self, retry=False): def cull(self, retry=False):
"""Cull items from cache until volume is less than size limit. """Cull items from cache until volume is less than size limit.
@ -474,7 +464,6 @@ class FanoutCache(object):
""" """
return self._remove('cull', retry=retry) return self._remove('cull', retry=retry)
def clear(self, retry=False): def clear(self, retry=False):
"""Remove all items from cache. """Remove all items from cache.
@ -487,7 +476,6 @@ class FanoutCache(object):
""" """
return self._remove('clear', retry=retry) return self._remove('clear', retry=retry)
def _remove(self, name, args=(), retry=False): def _remove(self, name, args=(), retry=False):
total = 0 total = 0
for shard in self._shards: for shard in self._shards:
@ -502,7 +490,6 @@ class FanoutCache(object):
break break
return total return total
def stats(self, enable=True, reset=False): def stats(self, enable=True, reset=False):
"""Return cache statistics hits and misses. """Return cache statistics hits and misses.
@ -516,7 +503,6 @@ class FanoutCache(object):
total_misses = sum(misses for _, misses in results) total_misses = sum(misses for _, misses in results)
return total_hits, total_misses return total_hits, total_misses
def volume(self): def volume(self):
"""Return estimated total size of cache on disk. """Return estimated total size of cache on disk.
@ -525,49 +511,40 @@ class FanoutCache(object):
""" """
return sum(shard.volume() for shard in self._shards) return sum(shard.volume() for shard in self._shards)
def close(self): def close(self):
"Close database connection." """Close database connection."""
for shard in self._shards: for shard in self._shards:
shard.close() shard.close()
self._caches.clear() self._caches.clear()
self._deques.clear() self._deques.clear()
self._indexes.clear() self._indexes.clear()
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, *exception): def __exit__(self, *exception):
self.close() self.close()
def __getstate__(self): def __getstate__(self):
return (self._directory, self._count, self.timeout, type(self.disk)) return (self._directory, self._count, self.timeout, type(self.disk))
def __setstate__(self, state): def __setstate__(self, state):
self.__init__(*state) self.__init__(*state)
def __iter__(self): def __iter__(self):
"Iterate keys in cache including expired items." """Iterate keys in cache including expired items."""
iterators = (iter(shard) for shard in self._shards) iterators = (iter(shard) for shard in self._shards)
return it.chain.from_iterable(iterators) return it.chain.from_iterable(iterators)
def __reversed__(self): def __reversed__(self):
"Reverse iterate keys in cache including expired items." """Reverse iterate keys in cache including expired items."""
iterators = (reversed(shard) for shard in reversed(self._shards)) iterators = (reversed(shard) for shard in reversed(self._shards))
return it.chain.from_iterable(iterators) return it.chain.from_iterable(iterators)
def __len__(self): def __len__(self):
"Count of items in cache including expired items." """Count of items in cache including expired items."""
return sum(len(shard) for shard in self._shards) return sum(len(shard) for shard in self._shards)
def reset(self, key, value=ENOVAL): def reset(self, key, value=ENOVAL):
"""Reset `key` and `value` item from Settings table. """Reset `key` and `value` item from Settings table.
@ -596,7 +573,6 @@ class FanoutCache(object):
break break
return result return result
def cache(self, name): def cache(self, name):
"""Return Cache with given `name` in subdirectory. """Return Cache with given `name` in subdirectory.
@ -622,11 +598,10 @@ class FanoutCache(object):
except KeyError: except KeyError:
parts = name.split('/') parts = name.split('/')
directory = op.join(self._directory, 'cache', *parts) directory = op.join(self._directory, 'cache', *parts)
temp = Cache(directory=directory) temp = Cache(directory=directory, disk=self._disk)
_caches[name] = temp _caches[name] = temp
return temp return temp
def deque(self, name): def deque(self, name):
"""Return Deque with given `name` in subdirectory. """Return Deque with given `name` in subdirectory.
@ -651,10 +626,10 @@ class FanoutCache(object):
except KeyError: except KeyError:
parts = name.split('/') parts = name.split('/')
directory = op.join(self._directory, 'deque', *parts) directory = op.join(self._directory, 'deque', *parts)
temp = Deque(directory=directory) cache = Cache(directory=directory, disk=self._disk)
_deques[name] = temp deque = Deque.fromcache(cache)
return temp _deques[name] = deque
return deque
def index(self, name): def index(self, name):
"""Return Index with given `name` in subdirectory. """Return Index with given `name` in subdirectory.
@ -683,9 +658,10 @@ class FanoutCache(object):
except KeyError: except KeyError:
parts = name.split('/') parts = name.split('/')
directory = op.join(self._directory, 'index', *parts) directory = op.join(self._directory, 'index', *parts)
temp = Index(directory) cache = Cache(directory=directory, disk=self._disk)
_indexes[name] = temp index = Index.fromcache(cache)
return temp _indexes[name] = index
return index
FanoutCache.memoize = Cache.memoize FanoutCache.memoize = Cache.memoize # type: ignore

View file

@ -1,22 +1,27 @@
"""Persistent Data Types """Persistent Data Types
""" """
import operator as op import operator as op
import sys
from collections import OrderedDict from collections import OrderedDict
from collections.abc import MutableMapping, Sequence from collections.abc import (
from collections.abc import KeysView, ValuesView, ItemsView ItemsView,
KeysView,
MutableMapping,
Sequence,
ValuesView,
)
from contextlib import contextmanager from contextlib import contextmanager
from shutil import rmtree from shutil import rmtree
from .core import Cache, ENOVAL from .core import ENOVAL, Cache
def _make_compare(seq_op, doc): def _make_compare(seq_op, doc):
"Make compare method with Sequence semantics." """Make compare method with Sequence semantics."""
def compare(self, that): def compare(self, that):
"Compare method for deque and sequence." """Compare method for deque and sequence."""
if not isinstance(that, Sequence): if not isinstance(that, Sequence):
return NotImplemented return NotImplemented
@ -70,6 +75,7 @@ class Deque(Sequence):
[3, 2, 1, 0, 0, -1, -2, -3] [3, 2, 1, 0, 0, -1, -2, -3]
""" """
def __init__(self, iterable=(), directory=None): def __init__(self, iterable=(), directory=None):
"""Initialize deque instance. """Initialize deque instance.
@ -81,9 +87,7 @@ class Deque(Sequence):
""" """
self._cache = Cache(directory, eviction_policy='none') self._cache = Cache(directory, eviction_policy='none')
with self.transact(): self.extend(iterable)
self.extend(iterable)
@classmethod @classmethod
def fromcache(cls, cache, iterable=()): def fromcache(cls, cache, iterable=()):
@ -111,19 +115,16 @@ class Deque(Sequence):
self.extend(iterable) self.extend(iterable)
return self return self
@property @property
def cache(self): def cache(self):
"Cache used by deque." """Cache used by deque."""
return self._cache return self._cache
@property @property
def directory(self): def directory(self):
"Directory path where deque is stored." """Directory path where deque is stored."""
return self._cache.directory return self._cache.directory
def _index(self, index, func): def _index(self, index, func):
len_self = len(self) len_self = len(self)
@ -154,7 +155,6 @@ class Deque(Sequence):
raise IndexError('deque index out of range') raise IndexError('deque index out of range')
def __getitem__(self, index): def __getitem__(self, index):
"""deque.__getitem__(index) <==> deque[index] """deque.__getitem__(index) <==> deque[index]
@ -177,7 +177,6 @@ class Deque(Sequence):
""" """
return self._index(index, self._cache.__getitem__) return self._index(index, self._cache.__getitem__)
def __setitem__(self, index, value): def __setitem__(self, index, value):
"""deque.__setitem__(index, value) <==> deque[index] = value """deque.__setitem__(index, value) <==> deque[index] = value
@ -196,10 +195,11 @@ class Deque(Sequence):
:raises IndexError: if index out of range :raises IndexError: if index out of range
""" """
def _set_value(key): def _set_value(key):
return self._cache.__setitem__(key, value) return self._cache.__setitem__(key, value)
self._index(index, _set_value)
self._index(index, _set_value)
def __delitem__(self, index): def __delitem__(self, index):
"""deque.__delitem__(index) <==> del deque[index] """deque.__delitem__(index) <==> del deque[index]
@ -220,7 +220,6 @@ class Deque(Sequence):
""" """
self._index(index, self._cache.__delitem__) self._index(index, self._cache.__delitem__)
def __repr__(self): def __repr__(self):
"""deque.__repr__() <==> repr(deque) """deque.__repr__() <==> repr(deque)
@ -230,7 +229,6 @@ class Deque(Sequence):
name = type(self).__name__ name = type(self).__name__
return '{0}(directory={1!r})'.format(name, self.directory) return '{0}(directory={1!r})'.format(name, self.directory)
__eq__ = _make_compare(op.eq, 'equal to') __eq__ = _make_compare(op.eq, 'equal to')
__ne__ = _make_compare(op.ne, 'not equal to') __ne__ = _make_compare(op.ne, 'not equal to')
__lt__ = _make_compare(op.lt, 'less than') __lt__ = _make_compare(op.lt, 'less than')
@ -238,7 +236,6 @@ class Deque(Sequence):
__le__ = _make_compare(op.le, 'less than or equal to') __le__ = _make_compare(op.le, 'less than or equal to')
__ge__ = _make_compare(op.ge, 'greater than or equal to') __ge__ = _make_compare(op.ge, 'greater than or equal to')
def __iadd__(self, iterable): def __iadd__(self, iterable):
"""deque.__iadd__(iterable) <==> deque += iterable """deque.__iadd__(iterable) <==> deque += iterable
@ -251,7 +248,6 @@ class Deque(Sequence):
self.extend(iterable) self.extend(iterable)
return self return self
def __iter__(self): def __iter__(self):
"""deque.__iter__() <==> iter(deque) """deque.__iter__() <==> iter(deque)
@ -266,7 +262,6 @@ class Deque(Sequence):
except KeyError: except KeyError:
pass pass
def __len__(self): def __len__(self):
"""deque.__len__() <==> len(deque) """deque.__len__() <==> len(deque)
@ -275,7 +270,6 @@ class Deque(Sequence):
""" """
return len(self._cache) return len(self._cache)
def __reversed__(self): def __reversed__(self):
"""deque.__reversed__() <==> reversed(deque) """deque.__reversed__() <==> reversed(deque)
@ -298,15 +292,12 @@ class Deque(Sequence):
except KeyError: except KeyError:
pass pass
def __getstate__(self): def __getstate__(self):
return self.directory return self.directory
def __setstate__(self, state): def __setstate__(self, state):
self.__init__(directory=state) self.__init__(directory=state)
def append(self, value): def append(self, value):
"""Add `value` to back of deque. """Add `value` to back of deque.
@ -322,7 +313,6 @@ class Deque(Sequence):
""" """
self._cache.push(value, retry=True) self._cache.push(value, retry=True)
def appendleft(self, value): def appendleft(self, value):
"""Add `value` to front of deque. """Add `value` to front of deque.
@ -338,7 +328,6 @@ class Deque(Sequence):
""" """
self._cache.push(value, side='front', retry=True) self._cache.push(value, side='front', retry=True)
def clear(self): def clear(self):
"""Remove all elements from deque. """Remove all elements from deque.
@ -352,7 +341,6 @@ class Deque(Sequence):
""" """
self._cache.clear(retry=True) self._cache.clear(retry=True)
def count(self, value): def count(self, value):
"""Return number of occurrences of `value` in deque. """Return number of occurrences of `value` in deque.
@ -371,7 +359,6 @@ class Deque(Sequence):
""" """
return sum(1 for item in self if value == item) return sum(1 for item in self if value == item)
def extend(self, iterable): def extend(self, iterable):
"""Extend back side of deque with values from `iterable`. """Extend back side of deque with values from `iterable`.
@ -381,7 +368,6 @@ class Deque(Sequence):
for value in iterable: for value in iterable:
self.append(value) self.append(value)
def extendleft(self, iterable): def extendleft(self, iterable):
"""Extend front side of deque with value from `iterable`. """Extend front side of deque with value from `iterable`.
@ -396,7 +382,6 @@ class Deque(Sequence):
for value in iterable: for value in iterable:
self.appendleft(value) self.appendleft(value)
def peek(self): def peek(self):
"""Peek at value at back of deque. """Peek at value at back of deque.
@ -423,7 +408,6 @@ class Deque(Sequence):
raise IndexError('peek from an empty deque') raise IndexError('peek from an empty deque')
return value return value
def peekleft(self): def peekleft(self):
"""Peek at value at front of deque. """Peek at value at front of deque.
@ -450,7 +434,6 @@ class Deque(Sequence):
raise IndexError('peek from an empty deque') raise IndexError('peek from an empty deque')
return value return value
def pop(self): def pop(self):
"""Remove and return value at back of deque. """Remove and return value at back of deque.
@ -477,7 +460,6 @@ class Deque(Sequence):
raise IndexError('pop from an empty deque') raise IndexError('pop from an empty deque')
return value return value
def popleft(self): def popleft(self):
"""Remove and return value at front of deque. """Remove and return value at front of deque.
@ -502,7 +484,6 @@ class Deque(Sequence):
raise IndexError('pop from an empty deque') raise IndexError('pop from an empty deque')
return value return value
def remove(self, value): def remove(self, value):
"""Remove first occurrence of `value` in deque. """Remove first occurrence of `value` in deque.
@ -540,7 +521,6 @@ class Deque(Sequence):
raise ValueError('deque.remove(value): value not in deque') raise ValueError('deque.remove(value): value not in deque')
def reverse(self): def reverse(self):
"""Reverse deque in place. """Reverse deque in place.
@ -563,7 +543,6 @@ class Deque(Sequence):
del temp del temp
rmtree(directory) rmtree(directory)
def rotate(self, steps=1): def rotate(self, steps=1):
"""Rotate deque right by `steps`. """Rotate deque right by `steps`.
@ -612,9 +591,7 @@ class Deque(Sequence):
else: else:
self.append(value) self.append(value)
__hash__ = None # type: ignore
__hash__ = None
@contextmanager @contextmanager
def transact(self): def transact(self):
@ -665,6 +642,7 @@ class Index(MutableMapping):
('c', 3) ('c', 3)
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize index in directory and update items. """Initialize index in directory and update items.
@ -692,7 +670,6 @@ class Index(MutableMapping):
self._cache = Cache(directory, eviction_policy='none') self._cache = Cache(directory, eviction_policy='none')
self.update(*args, **kwargs) self.update(*args, **kwargs)
@classmethod @classmethod
def fromcache(cls, cache, *args, **kwargs): def fromcache(cls, cache, *args, **kwargs):
"""Initialize index using `cache` and update items. """Initialize index using `cache` and update items.
@ -720,19 +697,16 @@ class Index(MutableMapping):
self.update(*args, **kwargs) self.update(*args, **kwargs)
return self return self
@property @property
def cache(self): def cache(self):
"Cache used by index." """Cache used by index."""
return self._cache return self._cache
@property @property
def directory(self): def directory(self):
"Directory path where items are stored." """Directory path where items are stored."""
return self._cache.directory return self._cache.directory
def __getitem__(self, key): def __getitem__(self, key):
"""index.__getitem__(key) <==> index[key] """index.__getitem__(key) <==> index[key]
@ -756,7 +730,6 @@ class Index(MutableMapping):
""" """
return self._cache[key] return self._cache[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""index.__setitem__(key, value) <==> index[key] = value """index.__setitem__(key, value) <==> index[key] = value
@ -774,7 +747,6 @@ class Index(MutableMapping):
""" """
self._cache[key] = value self._cache[key] = value
def __delitem__(self, key): def __delitem__(self, key):
"""index.__delitem__(key) <==> del index[key] """index.__delitem__(key) <==> del index[key]
@ -797,7 +769,6 @@ class Index(MutableMapping):
""" """
del self._cache[key] del self._cache[key]
def setdefault(self, key, default=None): def setdefault(self, key, default=None):
"""Set and get value for `key` in index using `default`. """Set and get value for `key` in index using `default`.
@ -822,7 +793,6 @@ class Index(MutableMapping):
except KeyError: except KeyError:
_cache.add(key, default, retry=True) _cache.add(key, default, retry=True)
def peekitem(self, last=True): def peekitem(self, last=True):
"""Peek at key and value item pair in index based on iteration order. """Peek at key and value item pair in index based on iteration order.
@ -841,7 +811,6 @@ class Index(MutableMapping):
""" """
return self._cache.peekitem(last, retry=True) return self._cache.peekitem(last, retry=True)
def pop(self, key, default=ENOVAL): def pop(self, key, default=ENOVAL):
"""Remove corresponding item for `key` from index and return value. """Remove corresponding item for `key` from index and return value.
@ -872,7 +841,6 @@ class Index(MutableMapping):
raise KeyError(key) raise KeyError(key)
return value return value
def popitem(self, last=True): def popitem(self, last=True):
"""Remove and return item pair. """Remove and return item pair.
@ -907,7 +875,6 @@ class Index(MutableMapping):
return key, value return key, value
def push(self, value, prefix=None, side='back'): def push(self, value, prefix=None, side='back'):
"""Push `value` onto `side` of queue in index identified by `prefix`. """Push `value` onto `side` of queue in index identified by `prefix`.
@ -939,7 +906,6 @@ class Index(MutableMapping):
""" """
return self._cache.push(value, prefix, side, retry=True) return self._cache.push(value, prefix, side, retry=True)
def pull(self, prefix=None, default=(None, None), side='front'): def pull(self, prefix=None, default=(None, None), side='front'):
"""Pull key and value item pair from `side` of queue in index. """Pull key and value item pair from `side` of queue in index.
@ -980,7 +946,6 @@ class Index(MutableMapping):
""" """
return self._cache.pull(prefix, default, side, retry=True) return self._cache.pull(prefix, default, side, retry=True)
def clear(self): def clear(self):
"""Remove all items from index. """Remove all items from index.
@ -994,7 +959,6 @@ class Index(MutableMapping):
""" """
self._cache.clear(retry=True) self._cache.clear(retry=True)
def __iter__(self): def __iter__(self):
"""index.__iter__() <==> iter(index) """index.__iter__() <==> iter(index)
@ -1003,7 +967,6 @@ class Index(MutableMapping):
""" """
return iter(self._cache) return iter(self._cache)
def __reversed__(self): def __reversed__(self):
"""index.__reversed__() <==> reversed(index) """index.__reversed__() <==> reversed(index)
@ -1020,7 +983,6 @@ class Index(MutableMapping):
""" """
return reversed(self._cache) return reversed(self._cache)
def __len__(self): def __len__(self):
"""index.__len__() <==> len(index) """index.__len__() <==> len(index)
@ -1029,7 +991,6 @@ class Index(MutableMapping):
""" """
return len(self._cache) return len(self._cache)
def keys(self): def keys(self):
"""Set-like object providing a view of index keys. """Set-like object providing a view of index keys.
@ -1044,7 +1005,6 @@ class Index(MutableMapping):
""" """
return KeysView(self) return KeysView(self)
def values(self): def values(self):
"""Set-like object providing a view of index values. """Set-like object providing a view of index values.
@ -1059,7 +1019,6 @@ class Index(MutableMapping):
""" """
return ValuesView(self) return ValuesView(self)
def items(self): def items(self):
"""Set-like object providing a view of index items. """Set-like object providing a view of index items.
@ -1074,18 +1033,14 @@ class Index(MutableMapping):
""" """
return ItemsView(self) return ItemsView(self)
__hash__ = None # type: ignore
__hash__ = None
def __getstate__(self): def __getstate__(self):
return self.directory return self.directory
def __setstate__(self, state): def __setstate__(self, state):
self.__init__(state) self.__init__(state)
def __eq__(self, other): def __eq__(self, other):
"""index.__eq__(other) <==> index == other """index.__eq__(other) <==> index == other
@ -1119,7 +1074,6 @@ class Index(MutableMapping):
else: else:
return all(self[key] == other.get(key, ENOVAL) for key in self) return all(self[key] == other.get(key, ENOVAL) for key in self)
def __ne__(self, other): def __ne__(self, other):
"""index.__ne__(other) <==> index != other """index.__ne__(other) <==> index != other
@ -1143,8 +1097,7 @@ class Index(MutableMapping):
""" """
return not self == other return not self == other
def memoize(self, name=None, typed=False, ignore=()):
def memoize(self, name=None, typed=False):
"""Memoizing cache decorator. """Memoizing cache decorator.
Decorator to wrap callable with memoizing function using cache. Decorator to wrap callable with memoizing function using cache.
@ -1195,11 +1148,11 @@ class Index(MutableMapping):
:param str name: name given for callable (default None, automatic) :param str name: name given for callable (default None, automatic)
:param bool typed: cache different types separately (default False) :param bool typed: cache different types separately (default False)
:param set ignore: positional or keyword args to ignore (default ())
:return: callable decorator :return: callable decorator
""" """
return self._cache.memoize(name, typed) return self._cache.memoize(name, typed, ignore=ignore)
@contextmanager @contextmanager
def transact(self): def transact(self):
@ -1228,7 +1181,6 @@ class Index(MutableMapping):
with self._cache.transact(retry=True): with self._cache.transact(retry=True):
yield yield
def __repr__(self): def __repr__(self):
"""index.__repr__() <==> repr(index) """index.__repr__() <==> repr(index)

View file

@ -1,5 +1,4 @@
"""Disk Cache Recipes """Disk Cache Recipes
""" """
import functools import functools
@ -12,7 +11,7 @@ import time
from .core import ENOVAL, args_to_key, full_name from .core import ENOVAL, args_to_key, full_name
class Averager(object): class Averager:
"""Recipe for calculating a running average. """Recipe for calculating a running average.
Sometimes known as "online statistics," the running average maintains the Sometimes known as "online statistics," the running average maintains the
@ -32,6 +31,7 @@ class Averager(object):
None None
""" """
def __init__(self, cache, key, expire=None, tag=None): def __init__(self, cache, key, expire=None, tag=None):
self._cache = cache self._cache = cache
self._key = key self._key = key
@ -39,27 +39,30 @@ class Averager(object):
self._tag = tag self._tag = tag
def add(self, value): def add(self, value):
"Add `value` to average." """Add `value` to average."""
with self._cache.transact(retry=True): with self._cache.transact(retry=True):
total, count = self._cache.get(self._key, default=(0.0, 0)) total, count = self._cache.get(self._key, default=(0.0, 0))
total += value total += value
count += 1 count += 1
self._cache.set( self._cache.set(
self._key, (total, count), expire=self._expire, tag=self._tag, self._key,
(total, count),
expire=self._expire,
tag=self._tag,
) )
def get(self): def get(self):
"Get current average or return `None` if count equals zero." """Get current average or return `None` if count equals zero."""
total, count = self._cache.get(self._key, default=(0.0, 0), retry=True) total, count = self._cache.get(self._key, default=(0.0, 0), retry=True)
return None if count == 0 else total / count return None if count == 0 else total / count
def pop(self): def pop(self):
"Return current average and delete key." """Return current average and delete key."""
total, count = self._cache.pop(self._key, default=(0.0, 0), retry=True) total, count = self._cache.pop(self._key, default=(0.0, 0), retry=True)
return None if count == 0 else total / count return None if count == 0 else total / count
class Lock(object): class Lock:
"""Recipe for cross-process and cross-thread lock. """Recipe for cross-process and cross-thread lock.
>>> import diskcache >>> import diskcache
@ -71,6 +74,7 @@ class Lock(object):
... pass ... pass
""" """
def __init__(self, cache, key, expire=None, tag=None): def __init__(self, cache, key, expire=None, tag=None):
self._cache = cache self._cache = cache
self._key = key self._key = key
@ -78,7 +82,7 @@ class Lock(object):
self._tag = tag self._tag = tag
def acquire(self): def acquire(self):
"Acquire lock using spin-lock algorithm." """Acquire lock using spin-lock algorithm."""
while True: while True:
added = self._cache.add( added = self._cache.add(
self._key, self._key,
@ -92,11 +96,11 @@ class Lock(object):
time.sleep(0.001) time.sleep(0.001)
def release(self): def release(self):
"Release lock by deleting key." """Release lock by deleting key."""
self._cache.delete(self._key, retry=True) self._cache.delete(self._key, retry=True)
def locked(self): def locked(self):
"Return true if the lock is acquired." """Return true if the lock is acquired."""
return self._key in self._cache return self._key in self._cache
def __enter__(self): def __enter__(self):
@ -106,7 +110,7 @@ class Lock(object):
self.release() self.release()
class RLock(object): class RLock:
"""Recipe for cross-process and cross-thread re-entrant lock. """Recipe for cross-process and cross-thread re-entrant lock.
>>> import diskcache >>> import diskcache
@ -124,6 +128,7 @@ class RLock(object):
AssertionError: cannot release un-acquired lock AssertionError: cannot release un-acquired lock
""" """
def __init__(self, cache, key, expire=None, tag=None): def __init__(self, cache, key, expire=None, tag=None):
self._cache = cache self._cache = cache
self._key = key self._key = key
@ -131,7 +136,7 @@ class RLock(object):
self._tag = tag self._tag = tag
def acquire(self): def acquire(self):
"Acquire lock by incrementing count using spin-lock algorithm." """Acquire lock by incrementing count using spin-lock algorithm."""
pid = os.getpid() pid = os.getpid()
tid = threading.get_ident() tid = threading.get_ident()
pid_tid = '{}-{}'.format(pid, tid) pid_tid = '{}-{}'.format(pid, tid)
@ -141,14 +146,16 @@ class RLock(object):
value, count = self._cache.get(self._key, default=(None, 0)) value, count = self._cache.get(self._key, default=(None, 0))
if pid_tid == value or count == 0: if pid_tid == value or count == 0:
self._cache.set( self._cache.set(
self._key, (pid_tid, count + 1), self._key,
expire=self._expire, tag=self._tag, (pid_tid, count + 1),
expire=self._expire,
tag=self._tag,
) )
return return
time.sleep(0.001) time.sleep(0.001)
def release(self): def release(self):
"Release lock by decrementing count." """Release lock by decrementing count."""
pid = os.getpid() pid = os.getpid()
tid = threading.get_ident() tid = threading.get_ident()
pid_tid = '{}-{}'.format(pid, tid) pid_tid = '{}-{}'.format(pid, tid)
@ -158,8 +165,10 @@ class RLock(object):
is_owned = pid_tid == value and count > 0 is_owned = pid_tid == value and count > 0
assert is_owned, 'cannot release un-acquired lock' assert is_owned, 'cannot release un-acquired lock'
self._cache.set( self._cache.set(
self._key, (value, count - 1), self._key,
expire=self._expire, tag=self._tag, (value, count - 1),
expire=self._expire,
tag=self._tag,
) )
def __enter__(self): def __enter__(self):
@ -169,7 +178,7 @@ class RLock(object):
self.release() self.release()
class BoundedSemaphore(object): class BoundedSemaphore:
"""Recipe for cross-process and cross-thread bounded semaphore. """Recipe for cross-process and cross-thread bounded semaphore.
>>> import diskcache >>> import diskcache
@ -187,6 +196,7 @@ class BoundedSemaphore(object):
AssertionError: cannot release un-acquired semaphore AssertionError: cannot release un-acquired semaphore
""" """
def __init__(self, cache, key, value=1, expire=None, tag=None): def __init__(self, cache, key, value=1, expire=None, tag=None):
self._cache = cache self._cache = cache
self._key = key self._key = key
@ -195,26 +205,31 @@ class BoundedSemaphore(object):
self._tag = tag self._tag = tag
def acquire(self): def acquire(self):
"Acquire semaphore by decrementing value using spin-lock algorithm." """Acquire semaphore by decrementing value using spin-lock algorithm."""
while True: while True:
with self._cache.transact(retry=True): with self._cache.transact(retry=True):
value = self._cache.get(self._key, default=self._value) value = self._cache.get(self._key, default=self._value)
if value > 0: if value > 0:
self._cache.set( self._cache.set(
self._key, value - 1, self._key,
expire=self._expire, tag=self._tag, value - 1,
expire=self._expire,
tag=self._tag,
) )
return return
time.sleep(0.001) time.sleep(0.001)
def release(self): def release(self):
"Release semaphore by incrementing value." """Release semaphore by incrementing value."""
with self._cache.transact(retry=True): with self._cache.transact(retry=True):
value = self._cache.get(self._key, default=self._value) value = self._cache.get(self._key, default=self._value)
assert self._value > value, 'cannot release un-acquired semaphore' assert self._value > value, 'cannot release un-acquired semaphore'
value += 1 value += 1
self._cache.set( self._cache.set(
self._key, value, expire=self._expire, tag=self._tag, self._key,
value,
expire=self._expire,
tag=self._tag,
) )
def __enter__(self): def __enter__(self):
@ -224,8 +239,16 @@ class BoundedSemaphore(object):
self.release() self.release()
def throttle(cache, count, seconds, name=None, expire=None, tag=None, def throttle(
time_func=time.time, sleep_func=time.sleep): cache,
count,
seconds,
name=None,
expire=None,
tag=None,
time_func=time.time,
sleep_func=time.sleep,
):
"""Decorator to throttle calls to function. """Decorator to throttle calls to function.
>>> import diskcache, time >>> import diskcache, time
@ -242,6 +265,7 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None,
True True
""" """
def decorator(func): def decorator(func):
rate = count / float(seconds) rate = count / float(seconds)
key = full_name(func) if name is None else name key = full_name(func) if name is None else name
@ -298,6 +322,7 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None):
>>> pool.terminate() >>> pool.terminate()
""" """
def decorator(func): def decorator(func):
key = full_name(func) if name is None else name key = full_name(func) if name is None else name
lock = lock_factory(cache, key, expire=expire, tag=tag) lock = lock_factory(cache, key, expire=expire, tag=tag)
@ -312,7 +337,9 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None):
return decorator return decorator
def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): def memoize_stampede(
cache, expire, name=None, typed=False, tag=None, beta=1, ignore=()
):
"""Memoizing cache decorator with cache stampede protection. """Memoizing cache decorator with cache stampede protection.
Cache stampedes are a type of system overload that can occur when parallel Cache stampedes are a type of system overload that can occur when parallel
@ -365,16 +392,17 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
:param str name: name given for callable (default None, automatic) :param str name: name given for callable (default None, automatic)
:param bool typed: cache different types separately (default False) :param bool typed: cache different types separately (default False)
:param str tag: text to associate with arguments (default None) :param str tag: text to associate with arguments (default None)
:param set ignore: positional or keyword args to ignore (default ())
:return: callable decorator :return: callable decorator
""" """
# Caution: Nearly identical code exists in Cache.memoize # Caution: Nearly identical code exists in Cache.memoize
def decorator(func): def decorator(func):
"Decorator created by memoize call for callable." """Decorator created by memoize call for callable."""
base = (full_name(func),) if name is None else (name,) base = (full_name(func),) if name is None else (name,)
def timer(*args, **kwargs): def timer(*args, **kwargs):
"Time execution of `func` and return result and time delta." """Time execution of `func` and return result and time delta."""
start = time.time() start = time.time()
result = func(*args, **kwargs) result = func(*args, **kwargs)
delta = time.time() - start delta = time.time() - start
@ -382,10 +410,13 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
"Wrapper for callable to cache arguments and return values." """Wrapper for callable to cache arguments and return values."""
key = wrapper.__cache_key__(*args, **kwargs) key = wrapper.__cache_key__(*args, **kwargs)
pair, expire_time = cache.get( pair, expire_time = cache.get(
key, default=ENOVAL, expire_time=True, retry=True, key,
default=ENOVAL,
expire_time=True,
retry=True,
) )
if pair is not ENOVAL: if pair is not ENOVAL:
@ -400,7 +431,10 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
thread_key = key + (ENOVAL,) thread_key = key + (ENOVAL,)
thread_added = cache.add( thread_added = cache.add(
thread_key, None, expire=delta, retry=True, thread_key,
None,
expire=delta,
retry=True,
) )
if thread_added: if thread_added:
@ -409,8 +443,13 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
with cache: with cache:
pair = timer(*args, **kwargs) pair = timer(*args, **kwargs)
cache.set( cache.set(
key, pair, expire=expire, tag=tag, retry=True, key,
pair,
expire=expire,
tag=tag,
retry=True,
) )
thread = threading.Thread(target=recompute) thread = threading.Thread(target=recompute)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@ -422,8 +461,8 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1):
return pair[0] return pair[0]
def __cache_key__(*args, **kwargs): def __cache_key__(*args, **kwargs):
"Make key for cache given function arguments." """Make key for cache given function arguments."""
return args_to_key(base, args, kwargs, typed) return args_to_key(base, args, kwargs, typed, ignore)
wrapper.__cache_key__ = __cache_key__ wrapper.__cache_key__ = __cache_key__
return wrapper return wrapper