1+ """An easy to use decorator for persistent memoization.
2+
3+ Like `functools.lrucache`, but results can be saved in any format to any storage.
4+ """
5+
16import datetime as dt
27import functools
38import hashlib
1116
1217
1318def hash_it (* data ) -> str :
14- """Hashes all the data passed to it as args."""
19+ """Pickles and hashes all the data passed to it as args."""
1520 result = hashlib .md5 ()
1621
1722 for datum in data :
@@ -21,44 +26,23 @@ def hash_it(*data) -> str:
2126
2227
2328def is_async (fn ):
29+ """Checks if the function is async."""
2430 return inspect .iscoroutinefunction (fn ) and not inspect .isgeneratorfunction (fn )
2531
2632
2733class Cache :
28- def __init__ (self , serializer : Serializer = None , storage : Storage = None ):
29- self .serializer = serializer or CloudPickleSerializer ()
30- self .storage = storage or LocalFileStorage ()
31-
32- @staticmethod
33- def get (
34- key : str , serializer : Serializer , storage : Storage , deadline : dt .datetime
35- ) -> Any :
36- data = storage .read (key , deadline )
37- return serializer .loads (data )
38-
39- @staticmethod
40- def set (key : str , value : Any , serializer : Serializer , storage : Storage ) -> None :
41- data = serializer .dumps (value )
42- storage .write (key , data )
34+ """A cache that can be used to memoize functions."""
4335
44- @staticmethod
45- def get_key (
46- fn : callable ,
47- args : tuple ,
48- kwargs : dict ,
49- serializer : Serializer ,
50- ignore : Iterable [str ],
51- ) -> str :
52- """Get a cache key."""
53-
54- # Remove ignored arguments from the arguments tuple and kwargs dict
55- if ignore is not None :
56- kwargs = {k : v for k , v in kwargs .items () if k not in ignore }
36+ def __init__ (self , serializer : Serializer = None , storage : Storage = None ):
37+ """Initialize the cache.
5738
58- return hash_it (inspect .getsource (fn ), type (serializer ), args , kwargs )
39+ Args:
40+ serializer: The serializer to use. If not specified, CloudPickleSerializer is used.
41+ storage: The storage to use. If not specified, LocalFileStorage is used.
42+ """
5943
60- def get_filename ( self , fn : callable , key : str , serializer : Serializer ) -> str :
61- return f" { fn . __name__ } - { key } . { serializer . extension } "
44+ self . serializer = serializer or CloudPickleSerializer ()
45+ self . storage = storage or LocalFileStorage ()
6246
6347 def cache (
6448 self ,
@@ -86,37 +70,68 @@ def cache(
8670 Defaults to None.
8771 """
8872
89- def decorator (fn ):
73+ def _decorator (fn ):
9074 ser = serializer or self .serializer
9175 stor = storage or self .storage
9276
9377 @functools .wraps (fn )
94- def non_async_wrapper (* args , ** kwargs ):
95- key = self .get_key (fn , args , kwargs , ser , ignore )
96- key = self .get_filename (fn , key , ser )
78+ def _non_async_wrapper (* args , ** kwargs ):
79+ key = self ._get_key (fn , args , kwargs , ser , ignore )
80+ key = self ._get_filename (fn , key , ser )
9781 try :
9882 deadline = dt .datetime .now (dt .timezone .utc ) - ttl if ttl else None
99- return self .get (key , ser , stor , deadline )
83+ return self ._get (key , ser , stor , deadline )
10084 except (FileNotFoundError , CacheExpired ):
10185 value = fn (* args , ** kwargs )
102- self .set (key , value , ser , stor )
86+ self ._set (key , value , ser , stor )
10387 return value
10488
10589 @functools .wraps (fn )
106- async def async_wrapper (* args , ** kwargs ):
107- key = self .get_key (fn , args , kwargs , ser , ignore )
108- key = self .get_filename (fn , key , ser )
90+ async def _async_wrapper (* args , ** kwargs ):
91+ key = self ._get_key (fn , args , kwargs , ser , ignore )
92+ key = self ._get_filename (fn , key , ser )
10993 try :
11094 deadline = dt .datetime .now (dt .timezone .utc ) - ttl if ttl else None
111- return self .get (key , ser , stor , deadline )
95+ return self ._get (key , ser , stor , deadline )
11296 except (FileNotFoundError , CacheExpired ):
11397 value = await fn (* args , ** kwargs )
114- self .set (key , value , ser , stor )
98+ self ._set (key , value , ser , stor )
11599 return value
116100
117- return async_wrapper if is_async (fn ) else non_async_wrapper
101+ return _async_wrapper if is_async (fn ) else _non_async_wrapper
102+
103+ return _decorator
104+
105+ @staticmethod
106+ def _get (
107+ key : str , serializer : Serializer , storage : Storage , deadline : dt .datetime
108+ ) -> Any :
109+ data = storage .read (key , deadline )
110+ return serializer .loads (data )
111+
112+ @staticmethod
113+ def _set (key : str , value : Any , serializer : Serializer , storage : Storage ) -> None :
114+ data = serializer .dumps (value )
115+ storage .write (key , data )
116+
117+ @staticmethod
118+ def _get_key (
119+ fn : callable ,
120+ args : tuple ,
121+ kwargs : dict ,
122+ serializer : Serializer ,
123+ ignore : Iterable [str ],
124+ ) -> str :
125+ """Get a cache key."""
118126
119- return decorator
127+ # Remove ignored arguments from the arguments tuple and kwargs dict
128+ if ignore is not None :
129+ kwargs = {k : v for k , v in kwargs .items () if k not in ignore }
130+
131+ return hash_it (inspect .getsource (fn ), type (serializer ), args , kwargs )
132+
133+ def _get_filename (self , fn : callable , key : str , serializer : Serializer ) -> str :
134+ return f"{ fn .__name__ } -{ key } .{ serializer .extension } "
120135
121136
122137class NoCache :
@@ -138,15 +153,15 @@ def cache(*decorator_args, **decorator_kwargs):
138153 return its result without any caching.
139154 """
140155
141- def decorator (fn ):
156+ def _decorator (fn ):
142157 @functools .wraps (fn )
143- def non_async_wrapper (* args , ** kwargs ):
158+ def _non_async_wrapper (* args , ** kwargs ):
144159 return fn (* args , ** kwargs )
145160
146161 @functools .wraps (fn )
147- async def async_wrapper (* args , ** kwargs ):
162+ async def _async_wrapper (* args , ** kwargs ):
148163 return await fn (* args , ** kwargs )
149164
150- return async_wrapper if is_async (fn ) else non_async_wrapper
165+ return _async_wrapper if is_async (fn ) else _non_async_wrapper
151166
152- return decorator
167+ return _decorator
0 commit comments