Skip to content

Commit 5caa8fd

Browse files
committed
Remove early recomputation and move into core.Cache
1 parent c6703b6 commit 5caa8fd

7 files changed

Lines changed: 293 additions & 269 deletions

File tree

diskcache/core.py

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import warnings
2020
import zlib
2121

22-
from .memo import memoize
23-
2422
if sys.hexversion < 0x03000000:
2523
import cPickle as pickle # pylint: disable=import-error
2624
# ISSUE #25 Fix for http://bugs.python.org/issue10211
@@ -40,6 +38,16 @@
4038
INT_TYPES = (int,)
4139
io_open = open # pylint: disable=invalid-name
4240

41+
def full_name(func):
42+
"Return full name of `func` by adding the module and function name."
43+
try:
44+
# The __qualname__ attribute is only available in Python 3.3 and later.
45+
# GrantJ 2019-03-29 Remove after support for Python 2 is dropped.
46+
name = func.__qualname__
47+
except AttributeError:
48+
name = func.__name__
49+
return func.__module__ + '.' + name
50+
4351
try:
4452
WindowsError
4553
except NameError:
@@ -357,6 +365,34 @@ class EmptyDirWarning(UserWarning):
357365
"Warning used by Cache.check for empty directories."
358366

359367

368+
def args_to_key(base, args, kwargs, typed):
369+
"""Create cache key out of function arguments.
370+
371+
:param tuple base: base of key
372+
:param tuple args: function arguments
373+
:param dict kwargs: function keyword arguments
374+
:param bool typed: include types in cache key
375+
:return: cache key tuple
376+
377+
"""
378+
key = base + args
379+
380+
if kwargs:
381+
key += (ENOVAL,)
382+
sorted_items = sorted(kwargs.items())
383+
384+
for item in sorted_items:
385+
key += item
386+
387+
if typed:
388+
key += tuple(type(arg) for arg in args)
389+
390+
if kwargs:
391+
key += tuple(type(value) for _, value in sorted_items)
392+
393+
return key
394+
395+
360396
class Cache(object):
361397
"Disk and file backed cache."
362398
# pylint: disable=bad-continuation
@@ -1725,7 +1761,92 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False):
17251761
return key, value
17261762

17271763

1728-
memoize = memoize
1764+
def memoize(self, name=None, typed=False, expire=None, tag=None):
1765+
"""Memoizing cache decorator.
1766+
1767+
Decorator to wrap callable with memoizing function using cache.
1768+
Repeated calls with the same arguments will lookup result in cache and
1769+
avoid function evaluation.
1770+
1771+
If name is set to None (default), the callable name will be determined
1772+
automatically.
1773+
1774+
If typed is set to True, function arguments of different types will be
1775+
cached separately. For example, f(3) and f(3.0) will be treated as
1776+
distinct calls with distinct results.
1777+
1778+
The original underlying function is accessible through the __wrapped__
1779+
attribute. This is useful for introspection, for bypassing the cache,
1780+
or for rewrapping the function with a different cache.
1781+
1782+
>>> from diskcache import Cache
1783+
>>> cache = Cache()
1784+
>>> @cache.memoize(expire=1, tag='fib')
1785+
... def fibonacci(number):
1786+
... if number == 0:
1787+
... return 0
1788+
... elif number == 1:
1789+
... return 1
1790+
... else:
1791+
... return fibonacci(number - 1) + fibonacci(number - 2)
1792+
>>> print(fibonacci(100))
1793+
354224848179261915075
1794+
1795+
An additional `__cache_key__` attribute can be used to generate the
1796+
cache key used for the given arguments.
1797+
1798+
>>> key = fibonacci.__cache_key__(100)
1799+
>>> print(cache[key])
1800+
354224848179261915075
1801+
1802+
Remember to call memoize when decorating a callable. If you forget,
1803+
then a TypeError will occur. Note the lack of parenthenses after
1804+
memoize below:
1805+
1806+
>>> @cache.memoize
1807+
... def test():
1808+
... pass
1809+
Traceback (most recent call last):
1810+
...
1811+
TypeError: name cannot be callable
1812+
1813+
:param cache: cache to store callable arguments and return values
1814+
:param str name: name given for callable (default None, automatic)
1815+
:param bool typed: cache different types separately (default False)
1816+
:param float expire: seconds until arguments expire
1817+
(default None, no expiry)
1818+
:param str tag: text to associate with arguments (default None)
1819+
:return: callable decorator
1820+
1821+
"""
1822+
# Caution: Nearly identical code exists in DjangoCache.memoize
1823+
if callable(name):
1824+
raise TypeError('name cannot be callable')
1825+
1826+
def decorator(func):
1827+
"Decorator created by memoize() for callable `func`."
1828+
base = (full_name(func),) if name is None else (name,)
1829+
1830+
@wraps(func)
1831+
def wrapper(*args, **kwargs):
1832+
"Wrapper for callable to cache arguments and return values."
1833+
key = wrapper.__cache_key__(*args, **kwargs)
1834+
result = self.get(key, default=ENOVAL, retry=True)
1835+
1836+
if result is ENOVAL:
1837+
result = func(*args, **kwargs)
1838+
self.set(key, result, expire=expire, tag=tag, retry=True)
1839+
1840+
return result
1841+
1842+
def __cache_key__(*args, **kwargs):
1843+
"Make key for cache given function arguments."
1844+
return args_to_key(base, args, kwargs, typed)
1845+
1846+
wrapper.__cache_key__ = __cache_key__
1847+
return wrapper
1848+
1849+
return decorator
17291850

17301851

17311852
def check(self, fix=False, retry=False):

diskcache/djangocache.py

Lines changed: 15 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
# For older versions of Django simply use 300 seconds.
1313
DEFAULT_TIMEOUT = 300
1414

15+
from .core import ENOVAL, args_to_key, full_name
1516
from .fanout import FanoutCache
16-
from .memo import MARK, _args_to_key, full_name
1717

1818

1919
class DjangoCache(BaseCache):
@@ -361,7 +361,7 @@ def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT):
361361

362362

363363
def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None,
364-
typed=False, tag=None, early_recompute=False, time_func=time):
364+
typed=False, tag=None):
365365
"""Memoizing cache decorator.
366366
367367
Decorator to wrap callable with memoizing function using cache.
@@ -375,20 +375,6 @@ def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None,
375375
cached separately. For example, f(3) and f(3.0) will be treated as
376376
distinct calls with distinct results.
377377
378-
Cache stampedes are a type of cascading failure that can occur when
379-
parallel computing systems using memoization come under heavy
380-
load. This behaviour is sometimes also called dog-piling, cache miss
381-
storm, cache choking, or the thundering herd problem.
382-
383-
The memoization decorator includes cache stampede protection through
384-
the early recomputation parameter. When set to True (default False),
385-
the expire parameter must not be None. Early recomputation of results
386-
will occur probabilistically before expiration.
387-
388-
Early probabilistic recomputation is based on research by Vattani, A.;
389-
Chierichetti, F.; Lowenstein, K. (2015), Optimal Probabilistic Cache
390-
Stampede Prevention, VLDB, pp. 886?897, ISSN 2150-8097
391-
392378
The original underlying function is accessible through the __wrapped__
393379
attribute. This is useful for introspection, for bypassing the cache,
394380
or for rewrapping the function with a different cache.
@@ -405,60 +391,30 @@ def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None,
405391
:param int version: key version number (default None, cache parameter)
406392
:param bool typed: cache different types separately (default False)
407393
:param str tag: text to associate with arguments (default None)
408-
:param bool early_recompute: probabilistic early recomputation
409-
(default False)
410-
:param time_func: callable for calculating current time
411394
:return: callable decorator
412395
413396
"""
414-
# Caution: Nearly identical code exists in memo.memoize
397+
# Caution: Nearly identical code exists in Cache.memoize
415398
if callable(name):
416399
raise TypeError('name cannot be callable')
417400

418-
if early_recompute and timeout is None:
419-
raise ValueError('timeout required')
420-
421401
def decorator(func):
422-
"Decorator created by memoize call for callable."
402+
"Decorator created by memoize() for callable `func`."
423403
base = (full_name(func),) if name is None else (name,)
424404

425-
if early_recompute:
426-
@wraps(func)
427-
def wrapper(*args, **kwargs):
428-
"Wrapper for callable to cache arguments and return values."
429-
key = wrapper.__cache_key__(*args, **kwargs)
430-
pair, expire_time = self.get(
431-
key, MARK, version, expire_time=True, retry=True,
432-
)
433-
434-
if pair is not MARK:
435-
result, delta = pair
436-
now = time_func()
437-
ttl = expire_time - now
438-
439-
if (-delta * log(random())) < ttl:
440-
return result
405+
@wraps(func)
406+
def wrapper(*args, **kwargs):
407+
"Wrapper for callable to cache arguments and return values."
408+
key = wrapper.__cache_key__(*args, **kwargs)
409+
result = self.get(key, ENOVAL, version, retry=True)
441410

442-
start = time_func()
411+
if result is ENOVAL:
443412
result = func(*args, **kwargs)
444-
delta = time_func() - start
445-
pair = result, delta
446-
self.set(key, pair, timeout, version, tag=tag, retry=True)
447-
return result
448-
else:
449-
@wraps(func)
450-
def wrapper(*args, **kwargs):
451-
"Wrapper for callable to cache arguments and return values."
452-
key = wrapper.__cache_key__(*args, **kwargs)
453-
result = self.get(key, MARK, version, retry=True)
454-
455-
if result is MARK:
456-
result = func(*args, **kwargs)
457-
self.set(
458-
key, result, timeout, version, tag=tag, retry=True,
459-
)
460-
461-
return result
413+
self.set(
414+
key, result, timeout, version, tag=tag, retry=True,
415+
)
416+
417+
return result
462418

463419
def __cache_key__(*args, **kwargs):
464420
"Make key for cache given function arguments."

diskcache/fanout.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
reduce # pylint: disable=pointless-statement
1414

1515
from .core import ENOVAL, DEFAULT_SETTINGS, Cache, Disk, Timeout
16-
from .memo import memoize
1716
from .persistent import Deque, Index
1817

1918

@@ -356,7 +355,7 @@ def __delitem__(self, key):
356355
del shard[key]
357356

358357

359-
memoize = memoize
358+
memoize = Cache.memoize
360359

361360

362361
def check(self, fix=False, retry=False):

0 commit comments

Comments
 (0)