11from __future__ import annotations
22
33from collections .abc import Callable , Iterator , MutableMapping
4- from itertools import chain
5- from typing import ( # TODO import from collections.abc (needs Python >=3.9)
6- ItemsView ,
7- ValuesView ,
8- )
94
105from zict .common import KT , VT , ZictBase , close , discard , flush , locked
116from zict .lru import LRU
7+ from zict .utils import InsertionSortedSet
128
139
1410class Buffer (ZictBase [KT , VT ]):
@@ -34,6 +30,11 @@ class Buffer(ZictBase[KT, VT]):
3430 storing to disk and raised a disk full error) the key will remain in the LRU.
3531 slow_to_fast_callbacks: list of callables
3632 These functions run every time data moves form the slow to the fast mapping.
33+ keep_slow: bool, optional
34+ If False (default), delete key/value pairs in slow when they are moved back to
35+ fast.
36+ If True, keep them in slow until deleted; this will avoid repeating the fast to
37+ slow transition when they are evicted again, but at the cost of duplication.
3738
3839 Notes
3940 -----
@@ -60,7 +61,9 @@ class Buffer(ZictBase[KT, VT]):
6061 weight : Callable [[KT , VT ], float ]
6162 fast_to_slow_callbacks : list [Callable [[KT , VT ], None ]]
6263 slow_to_fast_callbacks : list [Callable [[KT , VT ], None ]]
64+ keep_slow : bool
6365 _cancel_restore : dict [KT , bool ]
66+ _keys : InsertionSortedSet [KT ]
6467
6568 def __init__ (
6669 self ,
@@ -74,6 +77,7 @@ def __init__(
7477 slow_to_fast_callbacks : Callable [[KT , VT ], None ]
7578 | list [Callable [[KT , VT ], None ]]
7679 | None = None ,
80+ keep_slow : bool = False ,
7781 ):
7882 super ().__init__ ()
7983 self .fast = LRU (
@@ -91,7 +95,9 @@ def __init__(
9195 slow_to_fast_callbacks = [slow_to_fast_callbacks ]
9296 self .fast_to_slow_callbacks = fast_to_slow_callbacks or []
9397 self .slow_to_fast_callbacks = slow_to_fast_callbacks or []
98+ self .keep_slow = keep_slow
9499 self ._cancel_restore = {}
100+ self ._keys = InsertionSortedSet ((* self .fast , * self .slow ))
95101
96102 @property
97103 def n (self ) -> float :
@@ -136,6 +142,9 @@ def offset(self, value: float) -> None:
136142 self .fast .offset = value
137143
138144 def fast_to_slow (self , key : KT , value : VT ) -> None :
145+ if self .keep_slow and key in self .slow :
146+ return
147+
139148 self .slow [key ] = value
140149 try :
141150 for cb in self .fast_to_slow_callbacks :
@@ -169,7 +178,8 @@ def slow_to_fast(self, key: KT) -> VT:
169178 # - If the below code was just `self.fast[key] = value; del
170179 # self.slow[key]` now the key would be in neither slow nor fast!
171180 self .fast .set_noevict (key , value )
172- del self .slow [key ]
181+ if not self .keep_slow :
182+ del self .slow [key ]
173183
174184 with self .unlock ():
175185 self .fast .evict_until_below_target ()
@@ -180,27 +190,31 @@ def slow_to_fast(self, key: KT) -> VT:
180190
181191 @locked
182192 def __getitem__ (self , key : KT ) -> VT :
193+ if key not in self ._keys :
194+ raise KeyError (key )
183195 try :
184196 return self .fast [key ]
185197 except KeyError :
186198 return self .slow_to_fast (key )
187199
188200 def __setitem__ (self , key : KT , value : VT ) -> None :
189- with self .lock :
190- discard (self .slow , key )
191- if key in self ._cancel_restore :
192- self ._cancel_restore [key ] = True
193- self .fast [key ] = value
201+ self .set_noevict (key , value )
202+ try :
203+ self .fast .evict_until_below_target ()
204+ except Exception :
205+ self .fast ._setitem_exception (key )
206+ raise
194207
195208 @locked
196209 def set_noevict (self , key : KT , value : VT ) -> None :
197210 """Variant of ``__setitem__`` that does not move keys from fast to slow if the
198211 total weight exceeds n
199212 """
200- discard (self . slow , key )
213+ discard (self , key )
201214 if key in self ._cancel_restore :
202215 self ._cancel_restore [key ] = True
203216 self .fast .set_noevict (key , value )
217+ self ._keys .add (key )
204218
205219 def evict_until_below_target (self , n : float | None = None ) -> None :
206220 """Wrapper around :meth:`zict.LRU.evict_until_below_target`.
@@ -210,55 +224,32 @@ def evict_until_below_target(self, n: float | None = None) -> None:
210224
211225 @locked
212226 def __delitem__ (self , key : KT ) -> None :
227+ self ._keys .remove (key )
213228 if key in self ._cancel_restore :
214229 self ._cancel_restore [key ] = True
215- try :
216- del self .fast [key ]
217- except KeyError :
218- del self .slow [key ]
230+ discard (self .fast , key )
231+ discard (self .slow , key )
219232
220233 @locked
221234 def _cancel_evict (self , key : KT , value : VT ) -> None :
222235 discard (self .slow , key )
223236
224- def values (self ) -> ValuesView [VT ]:
225- return BufferValuesView (self )
226-
227- def items (self ) -> ItemsView [KT , VT ]:
228- return BufferItemsView (self )
229-
230237 def __len__ (self ) -> int :
231- with self .lock , self .fast .lock :
232- return (
233- len (self .fast )
234- + len (self .slow )
235- - sum (
236- k in self .fast and k in self .slow
237- for k in chain (self ._cancel_restore , self .fast ._cancel_evict )
238- )
239- )
238+ return len (self ._keys )
240239
241240 def __iter__ (self ) -> Iterator [KT ]:
242- """Make sure that the iteration is not disrupted if you evict/restore a key in
243- the middle of it
244- """
245- seen = set ()
246- while True :
247- try :
248- for d in (self .fast , self .slow ):
249- for key in d :
250- if key not in seen :
251- seen .add (key )
252- yield key
253- return
254- except RuntimeError :
255- pass
241+ return iter (self ._keys )
256242
257243 def __contains__ (self , key : object ) -> bool :
258- return key in self .fast or key in self . slow
244+ return key in self ._keys
259245
246+ @locked
260247 def __str__ (self ) -> str :
261- return f"Buffer<{ self .fast } , { self .slow } >"
248+ s = f"Buffer<fast: { len (self .fast )} , slow: { len (self .slow )} "
249+ if self .keep_slow :
250+ ndup = len (self .fast ) + len (self .slow ) - len (self ._keys )
251+ s += f", unique: { len (self ._keys )} , duplicates: { ndup } "
252+ return s + ">"
262253
263254 __repr__ = __str__
264255
@@ -267,25 +258,3 @@ def flush(self) -> None:
267258
268259 def close (self ) -> None :
269260 close (self .fast , self .slow )
270-
271-
272- class BufferItemsView (ItemsView [KT , VT ]):
273- _mapping : Buffer # FIXME CPython implementation detail
274- __slots__ = ()
275-
276- def __iter__ (self ) -> Iterator [tuple [KT , VT ]]:
277- # Avoid changing the LRU
278- return chain (self ._mapping .fast .items (), self ._mapping .slow .items ())
279-
280-
281- class BufferValuesView (ValuesView [VT ]):
282- _mapping : Buffer # FIXME CPython implementation detail
283- __slots__ = ()
284-
285- def __contains__ (self , value : object ) -> bool :
286- # Avoid changing the LRU
287- return any (value == v for v in self )
288-
289- def __iter__ (self ) -> Iterator [VT ]:
290- # Avoid changing the LRU
291- return chain (self ._mapping .fast .values (), self ._mapping .slow .values ())
0 commit comments