Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ common \ list_dict.py: 90%

151 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-10 00:11 -0800

1# -*- coding: utf-8 -*- 

2""" 

3Created on Wed Nov 16 11:08:25 2022 

4 

5@author: jpeacock 

6""" 

7# ============================================================================= 

8# Imports 

9# ============================================================================= 

10from collections import OrderedDict 

11 

12 

13# ============================================================================= 

14 

15 

16class ListDict: 

17 """ 

18 Hack together an object that acts like a dictionary and list such that a 

19 user can get an item by index or key. 

20 

21 This is the first attempt, seems to work, might think about inheriting 

22 an OrderedDict and overloading. 

23 

24 """ 

25 

26 def __init__(self, values={}): 

27 self._home = OrderedDict(values) 

28 

29 def __str__(self): 

30 lines = ["Contents:", "-" * 12] 

31 for k, v in self._home.items(): 

32 lines.append(f"\t{k} = {v}") 

33 

34 return "\n".join(lines) 

35 

36 def __repr__(self): 

37 """ 

38 Return a string representation that is consistent across Python versions. 

39 """ 

40 if not self._home: 

41 return "ListDict({})" 

42 

43 # Create a consistent representation using standard dict format 

44 items = [] 

45 for key, value in self._home.items(): 

46 # Use repr() for both key and value to handle proper quoting 

47 items.append(f"{repr(key)}: {repr(value)}") 

48 

49 items_str = "{" + ", ".join(items) + "}" 

50 return f"ListDict({items_str})" 

51 

52 def __eq__(self, other): 

53 return self._home.__eq__(other._home) 

54 

55 def __len__(self): 

56 return self._home.__len__() 

57 

58 def _get_key_from_index(self, index): 

59 try: 

60 return next(key for ii, key in enumerate(self._home) if ii == index) 

61 

62 except StopIteration: 

63 raise KeyError(f"Could not find {index}") 

64 

65 def _get_index_from_key(self, key): 

66 try: 

67 return next(index for index, k in enumerate(self._home) if k == key) 

68 

69 except StopIteration: 

70 raise KeyError(f"Could not find {key}") 

71 

72 def _get_key_from_object(self, obj): 

73 """ 

74 Get the key from the metadata object 

75 

76 :param obj: DESCRIPTION 

77 :type obj: TYPE 

78 :return: DESCRIPTION 

79 :rtype: TYPE 

80 

81 """ 

82 if hasattr(obj, "id"): 

83 return obj.id 

84 elif hasattr(obj, "component"): 

85 return obj.component 

86 else: 

87 raise TypeError("could not identify an appropriate key from object") 

88 

89 def __deepcopy__(self, memodict={}): 

90 """ 

91 Need to skip copying the logger 

92 need to copy properties as well. 

93 

94 :return: DESCRIPTION 

95 :rtype: TYPE 

96 

97 """ 

98 copied = type(self)() 

99 for key, value in self.items(): 

100 if hasattr(value, "copy"): 

101 value = value.copy() 

102 copied[key] = value 

103 

104 return copied 

105 

106 def copy(self): 

107 """ 

108 Copy object 

109 

110 """ 

111 

112 return self.__deepcopy__() 

113 

114 def _get_index_slice_from_slice(self, items, key_slice): 

115 """ 

116 Get the slice index values from either an integer or key value 

117 

118 :param items: DESCRIPTION 

119 :type items: TYPE 

120 :param key_slice: DESCRIPTION 

121 :type key_slice: TYPE 

122 :raises TypeError: DESCRIPTION 

123 :return: DESCRIPTION 

124 :rtype: TYPE 

125 

126 """ 

127 if key_slice.start is None or isinstance(key_slice.start, int): 

128 start = key_slice.start 

129 elif isinstance(key_slice.start, str): 

130 start = self._get_index_from_key(key_slice.start) 

131 else: 

132 raise TypeError("Slice start must be type int or str") 

133 

134 if key_slice.stop is None or isinstance(key_slice.stop, int): 

135 stop = key_slice.stop 

136 elif isinstance(key_slice.stop, str): 

137 stop = self._get_index_from_key(key_slice.stop) 

138 else: 

139 raise TypeError("Slice stop must be type int or str") 

140 

141 return slice(start, stop, key_slice.step) 

142 

143 def __getitem__(self, value): 

144 if isinstance(value, str): 

145 try: 

146 return self._home[value] 

147 except KeyError: 

148 raise KeyError(f"Could not find {value}") 

149 

150 elif isinstance(value, int): 

151 key = self._get_key_from_index(value) 

152 return self._home[key] 

153 

154 elif isinstance(value, slice): 

155 return ListDict( 

156 list(self.items())[ 

157 self._get_index_slice_from_slice(self.items(), value) 

158 ] 

159 ) 

160 

161 else: 

162 raise TypeError("Index must be a string or integer value.") 

163 

164 def __setitem__(self, index, value): 

165 if isinstance(index, str): 

166 self._home[index] = value 

167 

168 elif isinstance(index, int): 

169 try: 

170 key = self._get_key_from_index(index) 

171 except KeyError: 

172 try: 

173 key = self._get_key_from_object(value) 

174 except TypeError: 

175 key = str(index) 

176 

177 self._home[key] = value 

178 

179 elif isinstance(index, slice): 

180 raise NotImplementedError( 

181 "Setting values from slice is not implemented yet" 

182 ) 

183 

184 def __iter__(self): 

185 return iter(self.values()) 

186 

187 def keys(self): 

188 return list(self._home.keys()) 

189 

190 def values(self): 

191 return list(self._home.values()) 

192 

193 def items(self): 

194 return self._home.items() 

195 

196 def append(self, obj): 

197 """ 

198 Append an object 

199 

200 :param obj: DESCRIPTION 

201 :type obj: TYPE 

202 :return: DESCRIPTION 

203 :rtype: TYPE 

204 

205 """ 

206 

207 try: 

208 key = self._get_key_from_object(obj) 

209 except TypeError: 

210 key = str(len(self.keys())) 

211 

212 self._home[key] = obj 

213 

214 def remove(self, key): 

215 """ 

216 remove an item based on key or index 

217 

218 :param key: DESCRIPTION 

219 :type key: TYPE 

220 :return: DESCRIPTION 

221 :rtype: TYPE 

222 

223 """ 

224 

225 if isinstance(key, str): 

226 self._home.__delitem__(key) 

227 

228 elif isinstance(key, int): 

229 key = self._get_key_from_index(key) 

230 self._home.__delitem__(key) 

231 elif key is None: 

232 try: 

233 self._home.__delitem__(key) 

234 except KeyError: 

235 raise (KeyError("Could not find None in keys.")) 

236 

237 else: 

238 raise TypeError("could not identify an appropriate key from object") 

239 

240 def extend(self, other, skip_keys=[]): 

241 """ 

242 extend the dictionary from another ListDict object 

243 

244 :param other: DESCRIPTION 

245 :type other: TYPE 

246 :return: DESCRIPTION 

247 :rtype: TYPE 

248 

249 """ 

250 if isinstance(skip_keys, str): 

251 skip_keys = [skip_keys] 

252 

253 if isinstance(other, (ListDict, dict, OrderedDict)): 

254 for key, value in other.items(): 

255 if key in skip_keys: 

256 continue 

257 self._home[key] = value 

258 

259 else: 

260 raise TypeError(f"Cannot extend from {type(other)}") 

261 

262 def sort(self, inplace=True): 

263 """ 

264 sort the dictionary keys into alphabetical order 

265 """ 

266 

267 od = OrderedDict() 

268 for key in sorted(self._home.keys()): 

269 od[key] = self._home[key] 

270 

271 if inplace: 

272 self._home = od 

273 else: 

274 return od 

275 

276 def update(self, other): 

277 """ 

278 Update from another ListDict 

279 """ 

280 

281 if not isinstance(other, (ListDict, dict, OrderedDict)): 

282 raise TypeError( 

283 f"Cannot update from {type(other)}, must be a " 

284 "ListDict, dict, OrderedDict" 

285 ) 

286 

287 self._home.update(other) 

288 

289 def pop(self, key): 

290 """ 

291 pop item off of dictionary. The key must be verbatim 

292 

293 :param key: key of item to be popped off of dictionary 

294 :type key: string 

295 :return: item popped 

296 

297 """ 

298 

299 if key in self.keys(): 

300 return dict([self._home.popitem(key)]) 

301 else: 

302 raise KeyError(f"{key} is not in ListDict keys.") 

303 

304 def update_keys(self): 

305 """ 

306 Update all keys in the ListDict based on the current state of the objects. 

307 

308 This is useful when object IDs have been changed after being added to the ListDict. 

309 The method will re-key all objects using their current ID values. 

310 

311 Returns 

312 ------- 

313 dict 

314 A dictionary mapping old keys to new keys for objects that were re-keyed. 

315 """ 

316 updates = {} 

317 new_home = OrderedDict() 

318 

319 for old_key, obj in self._home.items(): 

320 try: 

321 new_key = self._get_key_from_object(obj) 

322 except TypeError: 

323 # If we can't get a key from the object, keep the old key 

324 new_key = old_key 

325 

326 if old_key != new_key: 

327 updates[old_key] = new_key 

328 

329 new_home[new_key] = obj 

330 

331 self._home = new_home 

332 return updates 

333 

334 def to_dict(self, single=False, nested=False, required=False) -> None: 

335 """need to implement this method""" 

336 return None 

337 

338 def clear(self) -> None: 

339 """Clear all items from the ListDict.""" 

340 self._home.clear()