3838ItemIter = Iterable [Item ]
3939
4040
41+ class AttrNames :
42+ CollectionABC = {'__len__' , '__iter__' , '__contains__' }
43+ Mapping = CollectionABC | {'keys' , 'get' , 'items' , '__reversed__' , 'values' , '__getitem__' }
44+ MutableMapping = Mapping | {'setdefault' , 'pop' , 'popitem' , 'clear' , 'update' , '__delitem__' , '__setitem__' }
45+
46+ Collection = CollectionABC | {'head' }
47+ KvReader = (Mapping | {'head' }) - {'__reversed__' }
48+ KvPersister = (MutableMapping | {'head' }) - {'__reversed__' } - {'clear' }
49+
50+
4151class Collection (CollectionABC ):
52+ """The same as collections.abc.Collection, with some modifications:
53+ - Addition of a ``head``
54+ """
4255
4356 def __contains__ (self , x ) -> bool :
4457 """
@@ -96,28 +109,86 @@ class KvReader(Collection, Mapping):
96109
97110 def head (self ):
98111 for k , v in self .items ():
99- yield k , v
112+ return k , v
113+
114+ def __reversed__ (self ):
115+ """The __reversed__ is disabled at the base, but can be re-defined in subclasses.
116+ Rationale: KvReader is meant to wrap a variety of storage backends or key-value perspectives thereof.
117+ Not all of these would have a natural or intuitive order nor do we want to maintain one systematically.
118+
119+ If you need a reversed list, here's one way to do it, but note that it
120+ depends on how self iterates, which is not even assured to be consistent at every call:
121+ ```
122+ reversed = list(self)[::-1]
123+ ```
124+
125+ If the keys are comparable, therefore sortable, another natural option would be:
126+ ```
127+ reversed = sorted(self)[::-1]
128+ ```
129+ """
130+ raise NotImplementedError (__doc__ )
100131
101132
102133Reader = KvReader # alias
103134
104135
136+ # TODO: Should we really be using MutableMapping if we're disabling so many of it's methods?
105137# TODO: Wishful thinking: Define store type so the type is defined by it's methods, not by subclassing.
106- class Persister (Reader , MutableMapping ):
107- """ Acts as a MutableMapping abc, but disabling the clear method, and computing __len__ by counting keys"""
138+ class KvPersister (KvReader , MutableMapping ):
139+ """ Acts as a MutableMapping abc, but disabling the clear and __reversed__ method,
140+ and computing __len__ by iterating over all keys, and counting them.
141+
142+ Note that KvPersister is a MutableMapping, and as such, is dict-like.
143+ But that doesn't mean it's a dict.
144+
145+ For instance, consider the following code:
146+ ```
147+ s = SomeKvPersister()
148+ s['a']['b'] = 3
149+ ```
150+ If `s` is a dict, this would have the effect of adding a ('b', 3) item under 'a'.
151+ But in the general case, this might
152+ - fail, because the `s['a']` doesn't support sub-scripting (doesn't have a `__getitem__`)
153+ - or, worse, will pass silently but not actually persist the write as expected (e.g. LocalFileStore)
154+
155+ Another example: `s.popitem()` will pop a `(k, v)` pair off of the `s` store.
156+ That is, retrieve the `v` for `k`, delete the entry for `k`, and return a `(k, v)`.
157+ Note that unlike modern dicts which will return the last item that was stored
158+ -- that is, LIFO (last-in, first-out) order -- for KvPersisters,
159+ there's no assurance as to what item will be, since it will depend on the backend storage system
160+ and/or how the persister was implemented.
161+
162+ """
108163
109164 def clear (self ):
110- raise NotImplementedError ('''
111- The clear method was overridden to make dangerous difficult.
112- If you really want to delete all your data, you can do so by doing:
113- try:
114- while True:
115- self.popitem()
116- except KeyError:
117- pass''' )
165+ """The clear method is disabled to make dangerous difficult.
166+ You don't want to delete your whole DB
167+ If you really want to delete all your data, you can do so by doing something like this:
168+ ```
169+ for k in self:
170+ try:
171+ del self[k]
172+ except KeyError:
173+ pass
174+ ```
175+ """
176+ raise NotImplementedError (__doc__ )
118177
178+ # # TODO: Tests and documentation demos needed.
179+ # def popitem(self):
180+ # """pop a (k, v) pair off of the store.
181+ # That is, retrieve the v for k, delete the entry for k, and return a (k, v)
182+ # Note that unlike modern dicts which will return the last item that was stored
183+ # -- that is, LIFO (last-in, first-out) order -- for KvPersisters,
184+ # there's no assurance as to what item will be, since it will depend on the backend storage system
185+ # and/or how the persister was implemented.
186+ # :return:
187+ # """
188+ # return super(KvPersister, self).popitem()
119189
120- KvPersister = Persister # alias with explict name
190+
191+ Persister = KvPersister # alias for back-compatibility
121192
122193
123194# TODO: Make identity_func "identifiable". If we use the following one, we can use == to detect it's use,
@@ -136,7 +207,7 @@ class NoSuchItem():
136207no_such_item = NoSuchItem ()
137208
138209
139- class Store (Persister ):
210+ class Store (KvPersister ):
140211 """
141212 By store we mean key-value store. This could be files in a filesystem, objects in s3, or a database. Where and
142213 how the content is stored should be specified, but StoreInterface offers a dict-like interface to this.
@@ -245,9 +316,12 @@ def __init__(self, store=dict):
245316 _data_of_obj = static_identity_method
246317 _obj_of_data = static_identity_method
247318
319+ _max_repr_size = None
320+
248321 # Read ####################################################################
249322 def __getitem__ (self , k : Key ) -> Val :
250- return self ._obj_of_data (self .store .__getitem__ (self ._id_of_key (k )))
323+ return self ._obj_of_data (self .store [self ._id_of_key (k )])
324+ # return self._obj_of_data(self.store.__getitem__(self._id_of_key(k)))
251325
252326 def get (self , k : Key , default = None ) -> Val :
253327 if hasattr (self .store , 'get' ): # if store has a get method, use it
@@ -262,9 +336,20 @@ def get(self, k: Key, default=None) -> Val:
262336 else :
263337 return default
264338
339+ # def update(self, other=(), /, **kwds):
340+ # """
341+ # update(self, other=(), /, **kwds)
342+ # D.update([E, ]**F) -> None. Update D from mapping/iterable E and F.
343+ # If E present and has a .keys() method, does: for k in E: D[k] = E[k]
344+ # If E present and lacks .keys() method, does: for (k, v) in E: D[k] = v
345+ # In either case, this is followed by: for k, v in F.items(): D[k] = v
346+ # :return:
347+ # """
348+
265349 # Explore ####################################################################
266350 def __iter__ (self ) -> KeyIter :
267- return map (self ._key_of_id , self .store .__iter__ ())
351+ yield from (self ._key_of_id (k ) for k in self .store )
352+ # return map(self._key_of_id, self.store.__iter__())
268353
269354 # def items(self) -> ItemIter:
270355 # if hasattr(self.store, 'items'):
@@ -273,27 +358,33 @@ def __iter__(self) -> KeyIter:
273358 # yield from ((self._key_of_id(k), self._obj_of_data(self.store[k])) for k in self.store.__iter__())
274359
275360 def __len__ (self ) -> int :
276- return self .store .__len__ ()
361+ return len (self .store )
362+ # return self.store.__len__()
277363
278364 def __contains__ (self , k ) -> bool :
279- return self .store .__contains__ (self ._id_of_key (k ))
365+ return self ._id_of_key (k ) in self .store
366+ # return self.store.__contains__(self._id_of_key(k))
280367
281368 def head (self ) -> Item :
369+ k = None
282370 try :
283371 for k in self :
284372 return k , self [k ]
285373 except Exception as e :
286374
287375 from warnings import warn
288- msg = f"Couldn't get data for the key { k } . This could be be...\n "
289- msg += "... because it's not a store (just a collection, that doesn't have a __getitem__)\n "
290- msg += "... because there's a layer transforming outcoming keys that are not the ones the store actually " \
291- "uses? If you didn't wrap the store with the inverse ingoing keys transformation, " \
292- "that would happen.\n "
293- msg += "I'll ask the inner-layer what it's head is, but IT MAY NOT REFLECT the reality of your store " \
294- "if you have some filtering, caching etc."
295- msg += f"The error messages was: \n { e } "
296- warn (msg )
376+ if k is None :
377+ raise
378+ else :
379+ msg = f"Couldn't get data for the key { k } . This could be be...\n "
380+ msg += "... because it's not a store (just a collection, that doesn't have a __getitem__)\n "
381+ msg += "... because there's a layer transforming outcoming keys that are not the ones the store actually " \
382+ "uses? If you didn't wrap the store with the inverse ingoing keys transformation, " \
383+ "that would happen.\n "
384+ msg += "I'll ask the inner-layer what it's head is, but IT MAY NOT REFLECT the reality of your store " \
385+ "if you have some filtering, caching etc."
386+ msg += f"The error messages was: \n { e } "
387+ warn (msg )
297388
298389 for _id in self .store :
299390 return self ._key_of_id (_id ), self ._obj_of_data (self .store [_id ])
@@ -312,21 +403,28 @@ def __setitem__(self, k: Key, v: Val):
312403 def __delitem__ (self , k : Key ):
313404 return self .store .__delitem__ (self ._id_of_key (k ))
314405
315- def clear (self ):
316- raise NotImplementedError ('''
317- The clear method was overridden to make dangerous difficult.
318- If you really want to delete all your data, you can do so by doing:
319- try:
320- while True:
321- self.popitem()
322- except KeyError:
323- pass''' )
406+ # def clear(self):
407+ # raise NotImplementedError('''
408+ # The clear method was overridden to make dangerous difficult.
409+ # If you really want to delete all your data, you can do so by doing:
410+ # try:
411+ # while True:
412+ # self.popitem()
413+ # except KeyError:
414+ # pass''')
324415
325416 # Misc ####################################################################
326417 def __repr__ (self ):
327- return self .store .__repr__ ()
418+ x = repr (self .store )
419+ if isinstance (self ._max_repr_size , int ):
420+ half = int (self ._max_repr_size )
421+ if len (x ) > self ._max_repr_size :
422+ x = x [:half ] + ' ... ' + x [- half :]
423+ return x
424+ # return self.store.__repr__()
328425
329426
427+ # Store.register(dict) # TODO: Would this be a good idea? To make isinstance({}, Store) be True (though missing head())
330428KvStore = Store # alias with explict name
331429
332430
0 commit comments