Coverage for mcpgateway / plugins / framework / memory.py: 100%

90 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-11 07:10 +0000

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

2"""Location: ./mcpgateway/plugins/framework/memory.py 

3Copyright 2025 

4SPDX-License-Identifier: Apache-2.0 

5Authors: Fred Araujo 

6 

7Memory management utilities for plugin framework. 

8 

9This module provides copy-on-write data structures for efficient memory management 

10in plugin contexts. 

11""" 

12 

13# Standard 

14from typing import Any, Iterator, Optional, TypeVar 

15 

16T = TypeVar("T") 

17 

18 

19class CopyOnWriteDict(dict): 

20 """ 

21 A dictionary subclass that implements copy-on-write behavior. 

22 

23 Inherits from dict and layers modifications over an original dictionary 

24 without mutating the original. The dict itself stores modifications, while 

25 reads check the modifications first, then fall back to the original. 

26 

27 This is useful for plugin contexts where you want to isolate modifications 

28 without copying the entire original dictionary upfront. Since it subclasses 

29 dict, it's compatible with type checking and validation frameworks like Pydantic. 

30 

31 Example: 

32 >>> original = {"a": 1, "b": 2, "c": 3} 

33 >>> cow = CopyOnWriteDict(original) 

34 >>> isinstance(cow, dict) 

35 True 

36 >>> cow["a"] = 10 # Modification stored in dict 

37 >>> cow["d"] = 4 # New key stored in dict 

38 >>> del cow["b"] # Deletion tracked separately 

39 >>> cow["a"] 

40 10 

41 >>> "b" in cow 

42 False 

43 >>> original # Original unchanged 

44 {'a': 1, 'b': 2, 'c': 3} 

45 >>> cow.get_modifications() 

46 {'a': 10, 'd': 4} 

47 """ 

48 

49 def __init__(self, original: dict): 

50 """ 

51 Initialize a copy-on-write dictionary wrapper. 

52 

53 Args: 

54 original: The original dictionary to wrap. This will not be modified. 

55 """ 

56 # Initialize parent dict without any data 

57 # The parent dict (self via super()) will store modifications only 

58 super().__init__() 

59 self._original = original 

60 self._deleted = set() # Track keys that have been deleted 

61 

62 def __getitem__(self, key: Any) -> Any: 

63 """ 

64 Get an item from the dictionary. 

65 

66 Args: 

67 key: The key to look up. 

68 

69 Returns: 

70 The value associated with the key. 

71 

72 Raises: 

73 KeyError: If the key is not found or has been deleted. 

74 """ 

75 if key in self._deleted: 

76 raise KeyError(key) 

77 # Check modifications first (via super()), then original 

78 if super().__contains__(key): 

79 return super().__getitem__(key) 

80 if key in self._original: 

81 return self._original[key] 

82 raise KeyError(key) 

83 

84 def __setitem__(self, key: Any, value: Any) -> None: 

85 """ 

86 Set an item in the dictionary. 

87 

88 The modification is stored in the wrapper layer, not the original dict. 

89 

90 Args: 

91 key: The key to set. 

92 value: The value to associate with the key. 

93 """ 

94 super().__setitem__(key, value) # Store in modifications (parent dict) 

95 self._deleted.discard(key) # If we're setting it, it's not deleted 

96 

97 def __delitem__(self, key: Any) -> None: 

98 """ 

99 Delete an item from the dictionary. 

100 

101 The key is marked as deleted in the wrapper layer. 

102 

103 Args: 

104 key: The key to delete. 

105 

106 Raises: 

107 KeyError: If the key doesn't exist in the dictionary. 

108 """ 

109 if key not in self: 

110 raise KeyError(key) 

111 self._deleted.add(key) 

112 if super().__contains__(key): 

113 super().__delitem__(key) # Remove from modifications if present 

114 

115 def __contains__(self, key: Any) -> bool: 

116 """ 

117 Check if a key exists in the dictionary. 

118 

119 Args: 

120 key: The key to check. 

121 

122 Returns: 

123 True if the key exists and hasn't been deleted, False otherwise. 

124 """ 

125 if key in self._deleted: 

126 return False 

127 return super().__contains__(key) or key in self._original 

128 

129 def __len__(self) -> int: 

130 """ 

131 Get the number of items in the dictionary. 

132 

133 Returns: 

134 The count of non-deleted keys. 

135 """ 

136 # Get all keys from both modifications and original, excluding deleted 

137 all_keys = set(super().keys()) | set(self._original.keys()) 

138 return len(all_keys - self._deleted) 

139 

140 def __iter__(self) -> Iterator: 

141 """ 

142 Iterate over keys in the dictionary. 

143 

144 Yields keys in insertion order: first keys from the original dict (in their 

145 original order), then new keys from modifications (in their insertion order). 

146 

147 Yields: 

148 Keys that haven't been deleted. 

149 """ 

150 # First, yield keys from original (in original order) 

151 for key in self._original: 

152 if key not in self._deleted: 

153 yield key 

154 

155 # Then yield new keys from modifications (not in original) 

156 for key in super().__iter__(): 

157 if key not in self._original and key not in self._deleted: 

158 yield key 

159 

160 def __repr__(self) -> str: 

161 """ 

162 Get a string representation of the dictionary. 

163 

164 Returns: 

165 A string representation showing the current state. 

166 """ 

167 return f"CopyOnWriteDict({dict(self.items())})" 

168 

169 def get(self, key: Any, default: Optional[Any] = None) -> Any: 

170 """ 

171 Get an item with a default fallback. 

172 

173 Args: 

174 key: The key to look up. 

175 default: The value to return if the key is not found. 

176 

177 Returns: 

178 The value associated with the key, or default if not found/deleted. 

179 """ 

180 try: 

181 return self[key] 

182 except KeyError: 

183 return default 

184 

185 def keys(self): 

186 """ 

187 Get all non-deleted keys. 

188 

189 Returns: 

190 A generator of keys. 

191 """ 

192 return iter(self) 

193 

194 def values(self): 

195 """ 

196 Get all values for non-deleted keys. 

197 

198 Returns: 

199 A generator of values. 

200 """ 

201 return (self[k] for k in self) 

202 

203 def items(self): 

204 """ 

205 Get all key-value pairs for non-deleted keys. 

206 

207 Returns: 

208 A generator of (key, value) tuples. 

209 """ 

210 return ((k, self[k]) for k in self) 

211 

212 def copy(self) -> dict: 

213 """ 

214 Create a regular dictionary with all current key-value pairs. 

215 

216 Returns: 

217 A new dict containing the current state (original + modifications - deletions). 

218 """ 

219 return dict(self.items()) 

220 

221 def get_modifications(self) -> dict: 

222 """ 

223 Get only the modifications made to the wrapper. 

224 

225 This returns only the keys that were added or changed in the modification layer, 

226 not including values from the original dictionary that weren't modified. 

227 

228 Returns: 

229 A copy of the modifications dictionary. 

230 """ 

231 # The parent dict (super()) contains only modifications 

232 return dict(super().items()) 

233 

234 def get_deleted(self) -> set: 

235 """ 

236 Get the set of deleted keys. 

237 

238 Returns: 

239 A copy of the deleted keys set. 

240 """ 

241 return self._deleted.copy() 

242 

243 def has_modifications(self) -> bool: 

244 """ 

245 Check if any modifications have been made. 

246 

247 Returns: 

248 True if there are any modifications or deletions, False otherwise. 

249 """ 

250 # Check if parent dict has any entries (modifications) or if anything was deleted 

251 return super().__len__() > 0 or len(self._deleted) > 0 

252 

253 def update(self, other=None, **kwargs) -> None: 

254 """ 

255 Update the dictionary with key-value pairs from another mapping or iterable. 

256 

257 Args: 

258 other: A mapping or iterable of key-value pairs. 

259 **kwargs: Additional key-value pairs to update. 

260 

261 Examples: 

262 >>> cow = CopyOnWriteDict({"a": 1}) 

263 >>> cow.update({"b": 2, "c": 3}) 

264 >>> cow.update(d=4, e=5) 

265 >>> dict(cow.items()) 

266 {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} 

267 """ 

268 if other is not None: 

269 if hasattr(other, "items"): 

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

271 self[key] = value 

272 else: 

273 for key, value in other: 

274 self[key] = value 

275 for key, value in kwargs.items(): 

276 self[key] = value 

277 

278 def pop(self, key: Any, *args) -> Any: 

279 """ 

280 Remove and return the value for a key. 

281 

282 Args: 

283 key: The key to remove. 

284 *args: Optional default value if key is not found. 

285 

286 Returns: 

287 The value associated with the key. 

288 

289 Raises: 

290 KeyError: If key is not found and no default is provided. 

291 TypeError: If more than one default argument is provided. 

292 

293 Examples: 

294 >>> cow = CopyOnWriteDict({"a": 1, "b": 2}) 

295 >>> cow.pop("a") 

296 1 

297 >>> cow.pop("c", "default") 

298 'default' 

299 """ 

300 if len(args) > 1: 

301 raise TypeError(f"pop() accepts 1 or 2 arguments ({len(args) + 1} given)") 

302 

303 try: 

304 value = self[key] 

305 del self[key] 

306 return value 

307 except KeyError: 

308 if args: 

309 return args[0] 

310 raise 

311 

312 def setdefault(self, key: Any, default: Any = None) -> Any: 

313 """ 

314 Get a value, setting it to a default if not present. 

315 

316 Args: 

317 key: The key to look up. 

318 default: The default value to set if key is not present. 

319 

320 Returns: 

321 The value associated with the key (existing or newly set). 

322 

323 Examples: 

324 >>> cow = CopyOnWriteDict({"a": 1}) 

325 >>> cow.setdefault("a", 10) 

326 1 

327 >>> cow.setdefault("b", 2) 

328 2 

329 >>> cow["b"] 

330 2 

331 """ 

332 if key in self: 

333 return self[key] 

334 self[key] = default 

335 return default 

336 

337 def clear(self) -> None: 

338 """ 

339 Remove all items from the dictionary. 

340 

341 This marks all keys (from original and modifications) as deleted. 

342 

343 Examples: 

344 >>> cow = CopyOnWriteDict({"a": 1, "b": 2}) 

345 >>> cow.clear() 

346 >>> len(cow) 

347 0 

348 """ 

349 # Mark all current keys as deleted 

350 for key in list(self.keys()): 

351 self._deleted.add(key) 

352 # Clear modifications from parent dict 

353 super().clear() 

354 

355 

356def copyonwrite(o: T) -> T: 

357 """ 

358 Returns a copy-on-write wrapper of the original object. 

359 

360 Args: 

361 o: The object to wrap. Currently only supports dict objects. 

362 

363 Returns: 

364 A copy-on-write wrapper around the object. 

365 

366 Raises: 

367 TypeError: If the object type is not supported for copy-on-write wrapping. 

368 """ 

369 if isinstance(o, dict): 

370 return CopyOnWriteDict(o) 

371 raise TypeError(f"No copy-on-write wrapper available for {type(o)}")