2525import threading
2626import copy
2727import json
28- from typing import TYPE_CHECKING , Optional , Sequence , List , Union
28+ from typing import TYPE_CHECKING , Optional , Sequence , List , Union , Any
2929
3030import jsonpatch
3131import jsonpointer
@@ -97,13 +97,16 @@ def decorator(func):
9797 return func
9898 return decorator
9999
100+ _FLEX_KEY = str | int | None
100101
101- def key_path (path : Sequence [Union [str , int ]], key : Optional [str ]) -> str :
102- def to_str (x ):
102+ def key_path (path : Sequence [_FLEX_KEY ], key : _FLEX_KEY ) -> str :
103+ def to_str (x : _FLEX_KEY ) -> str :
104+ assert isinstance (x , _FLEX_KEY ), repr (x )
105+ assert x is not None
103106 if isinstance (x , int ):
104107 return str (int (x ))
105108 else :
106- assert isinstance (x , str )
109+ assert isinstance (x , str ), f"unexpected key type for: { x !r } "
107110 return x
108111 items = [to_str (x ) for x in path ]
109112 if key is not None :
@@ -113,15 +116,17 @@ def to_str(x):
113116class BaseStoredObject :
114117
115118 _db : 'JsonDB' = None
116- _key = None
117- _parent = None
118- _lock = None
119+ _key : _FLEX_KEY = None
120+ _parent : Optional [ 'BaseStoredObject' ] = None
121+ _lock : threading . RLock = None
119122
120123 def set_db (self , db ):
121124 self ._db = db
122125 self ._lock = self ._db .lock if self ._db else threading .RLock ()
123126
124- def set_parent (self , key , parent ):
127+ def set_parent (self , * , key : _FLEX_KEY , parent : Optional ['BaseStoredObject' ]) -> None :
128+ assert (key == "" ) == (parent is None ), f"{ key = !r} , { parent = !r} "
129+ assert isinstance (key , _FLEX_KEY ), repr (key )
125130 self ._key = key
126131 self ._parent = parent
127132
@@ -130,7 +135,7 @@ def lock(self):
130135 return self ._lock
131136
132137 @property
133- def path (self ) -> Sequence [str ] :
138+ def path (self ) -> Sequence [_FLEX_KEY ] | None :
134139 # return None iff we are pruned from root
135140 x = self
136141 s = [x ._key ]
@@ -142,23 +147,27 @@ def path(self) -> Sequence[str]:
142147 assert self ._db is not None
143148 return s
144149
145- def db_add (self , key , value ):
150+ def db_add (self , key : _FLEX_KEY , value ) -> None :
151+ assert isinstance (key , _FLEX_KEY ), repr (key )
146152 if self .path :
147153 self ._db .add (self .path , key , value )
148154
149- def db_replace (self , key , value ):
155+ def db_replace (self , key : _FLEX_KEY , value ) -> None :
156+ assert isinstance (key , _FLEX_KEY ), repr (key )
150157 if self .path :
151158 self ._db .replace (self .path , key , value )
152159
153- def db_remove (self , key ):
160+ def db_remove (self , key : _FLEX_KEY ) -> None :
161+ assert isinstance (key , _FLEX_KEY ), repr (key )
154162 if self .path :
155163 self ._db .remove (self .path , key )
156164
157165
158166class StoredObject (BaseStoredObject ):
159167 """for attr.s objects """
160168
161- def __setattr__ (self , key , value ):
169+ def __setattr__ (self , key : str , value ):
170+ assert isinstance (key , str ), repr (key )
162171 if self .path and not key .startswith ('_' ):
163172 if value != getattr (self , key ):
164173 self .db_replace (key , value )
@@ -185,7 +194,8 @@ def __init__(self, data: dict, db: 'JsonDB'):
185194 self .__setitem__ (k , v )
186195
187196 @locked
188- def __setitem__ (self , key , v ):
197+ def __setitem__ (self , key : _FLEX_KEY , v ) -> None :
198+ assert isinstance (key , _FLEX_KEY ), repr (key )
189199 is_new = key not in self
190200 # early return to prevent unnecessary disk writes
191201 if not is_new and self ._db and json .dumps (v , cls = self ._db .encoder ) == json .dumps (self [key ], cls = self ._db .encoder ):
@@ -204,18 +214,20 @@ def __setitem__(self, key, v):
204214 v .set_db (self ._db )
205215 # set parent
206216 if isinstance (v , BaseStoredObject ):
207- v .set_parent (key , self )
217+ v .set_parent (key = key , parent = self )
208218 # set item
209219 dict .__setitem__ (self , key , v )
210220 self .db_add (key , v ) if is_new else self .db_replace (key , v )
211221
212222 @locked
213- def __delitem__ (self , key ):
223+ def __delitem__ (self , key : _FLEX_KEY ) -> None :
224+ assert isinstance (key , _FLEX_KEY ), repr (key )
214225 dict .__delitem__ (self , key )
215226 self .db_remove (key )
216227
217228 @locked
218- def pop (self , key , v = _RaiseKeyError ):
229+ def pop (self , key : _FLEX_KEY , v = _RaiseKeyError ) -> Any :
230+ assert isinstance (key , _FLEX_KEY ), repr (key )
219231 if key not in self :
220232 if v is _RaiseKeyError :
221233 raise KeyError (key )
@@ -227,7 +239,8 @@ def pop(self, key, v=_RaiseKeyError):
227239 r ._parent = None
228240 return r
229241
230- def setdefault (self , key , default = None , / ):
242+ def setdefault (self , key : _FLEX_KEY , default = None , / ):
243+ assert isinstance (key , _FLEX_KEY ), repr (key )
231244 if key not in self :
232245 self .__setitem__ (key , default )
233246 return self [key ]
@@ -283,7 +296,7 @@ def __init__(
283296 data = self ._convert_dict ([], data )
284297 # convert dict to StoredDict
285298 self .data = StoredDict (data , self )
286- self .data .set_parent ('' , None )
299+ self .data .set_parent (key = '' , parent = None )
287300 # write file in case there was a db upgrade
288301 if self .storage and self .storage .file_exists ():
289302 self .write_and_force_consolidation ()
@@ -357,13 +370,16 @@ def add_patch(self, patch):
357370 self .pending_changes .append (json .dumps (patch , cls = self .encoder ))
358371 self .set_modified (True )
359372
360- def add (self , path , key , value ):
373+ def add (self , path , key : _FLEX_KEY , value ) -> None :
374+ assert isinstance (key , _FLEX_KEY ), repr (key )
361375 self .add_patch ({'op' : 'add' , 'path' : key_path (path , key ), 'value' : value })
362376
363- def replace (self , path , key , value ):
377+ def replace (self , path , key : _FLEX_KEY , value ) -> None :
378+ assert isinstance (key , _FLEX_KEY ), repr (key )
364379 self .add_patch ({'op' : 'replace' , 'path' : key_path (path , key ), 'value' : value })
365380
366- def remove (self , path , key ):
381+ def remove (self , path , key : _FLEX_KEY ) -> None :
382+ assert isinstance (key , _FLEX_KEY ), repr (key )
367383 self .add_patch ({'op' : 'remove' , 'path' : key_path (path , key )})
368384
369385 @locked
@@ -419,7 +435,9 @@ def dump(self, *, human_readable: bool = True) -> str:
419435 def _should_convert_to_stored_dict (self , key ) -> bool :
420436 return True
421437
422- def _convert_dict_key (self , path ):
438+ def _convert_dict_key (self , path : List [str ]) -> _FLEX_KEY :
439+ """Maybe convert key from str to python type (typically int or IntEnum)"""
440+ assert all (isinstance (x , str ) for x in path ), repr (path )
423441 key = path [- 1 ]
424442 parent_key = path [- 2 ] if len (path ) > 1 else None
425443 gp_key = path [- 3 ] if len (path ) > 2 else None
@@ -431,9 +449,11 @@ def _convert_dict_key(self, path):
431449 convert_key = None
432450 if convert_key :
433451 key = convert_key (key )
452+ assert isinstance (key , _FLEX_KEY ), f"unexpected type for { key = !r} at { path = } "
434453 return key
435454
436- def _convert_dict_value (self , path , v ):
455+ def _convert_dict_value (self , path : List [str ], v ) -> Any :
456+ assert all (isinstance (x , str ) for x in path ), repr (path )
437457 key = path [- 1 ]
438458 if key in registered_dicts :
439459 constructor , _type = registered_dicts [key ]
@@ -453,8 +473,9 @@ def _convert_dict_value(self, path, v):
453473 v = self ._convert_dict (path , v )
454474 return v
455475
456- def _convert_dict (self , path , data : dict ):
457- # recursively convert dict to StoredDict
476+ def _convert_dict (self , path : List [str ], data : dict ):
477+ # recursively convert json dict to StoredDict
478+ assert all (isinstance (x , str ) for x in path ), repr (path )
458479 d = {}
459480 for k , v in list (data .items ()):
460481 child_path = path + [k ]
0 commit comments