Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/nbformat/_struct.py: 30%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

77 statements  

1"""A dict subclass that supports attribute style access. 

2 

3Can probably be replaced by types.SimpleNamespace from Python 3.3 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import Any, Dict 

9 

10__all__ = ["Struct"] 

11 

12 

13class Struct(Dict[Any, Any]): 

14 """A dict subclass with attribute style access. 

15 

16 This dict subclass has a a few extra features: 

17 

18 * Attribute style access. 

19 * Protection of class members (like keys, items) when using attribute 

20 style access. 

21 * The ability to restrict assignment to only existing keys. 

22 * Intelligent merging. 

23 * Overloaded operators. 

24 """ 

25 

26 _allownew = True 

27 

28 def __init__(self, *args, **kw): 

29 """Initialize with a dictionary, another Struct, or data. 

30 

31 Parameters 

32 ---------- 

33 *args : dict, Struct 

34 Initialize with one dict or Struct 

35 **kw : dict 

36 Initialize with key, value pairs. 

37 

38 Examples 

39 -------- 

40 >>> s = Struct(a=10,b=30) 

41 >>> s.a 

42 10 

43 >>> s.b 

44 30 

45 >>> s2 = Struct(s,c=30) 

46 >>> sorted(s2.keys()) 

47 ['a', 'b', 'c'] 

48 """ 

49 object.__setattr__(self, "_allownew", True) 

50 dict.__init__(self, *args, **kw) 

51 

52 def __setitem__(self, key, value): 

53 """Set an item with check for allownew. 

54 

55 Examples 

56 -------- 

57 >>> s = Struct() 

58 >>> s['a'] = 10 

59 >>> s.allow_new_attr(False) 

60 >>> s['a'] = 10 

61 >>> s['a'] 

62 10 

63 >>> try: 

64 ... s['b'] = 20 

65 ... except KeyError: 

66 ... print('this is not allowed') 

67 ... 

68 this is not allowed 

69 """ 

70 if not self._allownew and key not in self: 

71 raise KeyError("can't create new attribute %s when allow_new_attr(False)" % key) 

72 dict.__setitem__(self, key, value) 

73 

74 def __setattr__(self, key, value): 

75 """Set an attr with protection of class members. 

76 

77 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to 

78 :exc:`AttributeError`. 

79 

80 Examples 

81 -------- 

82 >>> s = Struct() 

83 >>> s.a = 10 

84 >>> s.a 

85 10 

86 >>> try: 

87 ... s.get = 10 

88 ... except AttributeError: 

89 ... print("you can't set a class member") 

90 ... 

91 you can't set a class member 

92 """ 

93 # If key is an str it might be a class member or instance var 

94 if isinstance(key, str): # noqa: SIM102 

95 # I can't simply call hasattr here because it calls getattr, which 

96 # calls self.__getattr__, which returns True for keys in 

97 # self._data. But I only want keys in the class and in 

98 # self.__dict__ 

99 if key in self.__dict__ or hasattr(Struct, key): 

100 raise AttributeError("attr %s is a protected member of class Struct." % key) 

101 try: 

102 self.__setitem__(key, value) 

103 except KeyError as e: 

104 raise AttributeError(e) from None 

105 

106 def __getattr__(self, key): 

107 """Get an attr by calling :meth:`dict.__getitem__`. 

108 

109 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to 

110 :exc:`AttributeError`. 

111 

112 Examples 

113 -------- 

114 >>> s = Struct(a=10) 

115 >>> s.a 

116 10 

117 >>> type(s.get) 

118 <... 'builtin_function_or_method'> 

119 >>> try: 

120 ... s.b 

121 ... except AttributeError: 

122 ... print("I don't have that key") 

123 ... 

124 I don't have that key 

125 """ 

126 try: 

127 result = self[key] 

128 except KeyError: 

129 raise AttributeError(key) from None 

130 else: 

131 return result 

132 

133 def __iadd__(self, other): 

134 """s += s2 is a shorthand for s.merge(s2). 

135 

136 Examples 

137 -------- 

138 >>> s = Struct(a=10,b=30) 

139 >>> s2 = Struct(a=20,c=40) 

140 >>> s += s2 

141 >>> sorted(s.keys()) 

142 ['a', 'b', 'c'] 

143 """ 

144 self.merge(other) 

145 return self 

146 

147 def __add__(self, other): 

148 """s + s2 -> New Struct made from s.merge(s2). 

149 

150 Examples 

151 -------- 

152 >>> s1 = Struct(a=10,b=30) 

153 >>> s2 = Struct(a=20,c=40) 

154 >>> s = s1 + s2 

155 >>> sorted(s.keys()) 

156 ['a', 'b', 'c'] 

157 """ 

158 sout = self.copy() 

159 sout.merge(other) 

160 return sout 

161 

162 def __sub__(self, other): 

163 """s1 - s2 -> remove keys in s2 from s1. 

164 

165 Examples 

166 -------- 

167 >>> s1 = Struct(a=10,b=30) 

168 >>> s2 = Struct(a=40) 

169 >>> s = s1 - s2 

170 >>> s 

171 {'b': 30} 

172 """ 

173 sout = self.copy() 

174 sout -= other 

175 return sout 

176 

177 def __isub__(self, other): 

178 """Inplace remove keys from self that are in other. 

179 

180 Examples 

181 -------- 

182 >>> s1 = Struct(a=10,b=30) 

183 >>> s2 = Struct(a=40) 

184 >>> s1 -= s2 

185 >>> s1 

186 {'b': 30} 

187 """ 

188 for k in other: 

189 if k in self: 

190 del self[k] 

191 return self 

192 

193 def __dict_invert(self, data): 

194 """Helper function for merge. 

195 

196 Takes a dictionary whose values are lists and returns a dict with 

197 the elements of each list as keys and the original keys as values. 

198 """ 

199 outdict = {} 

200 for k, lst in data.items(): 

201 if isinstance(lst, str): 

202 lst = lst.split() # noqa: PLW2901 

203 for entry in lst: 

204 outdict[entry] = k 

205 return outdict 

206 

207 def dict(self): 

208 """Get the dict representation of the struct.""" 

209 return self 

210 

211 def copy(self): 

212 """Return a copy as a Struct. 

213 

214 Examples 

215 -------- 

216 >>> s = Struct(a=10,b=30) 

217 >>> s2 = s.copy() 

218 >>> type(s2) is Struct 

219 True 

220 """ 

221 return Struct(dict.copy(self)) 

222 

223 def hasattr(self, key): 

224 """hasattr function available as a method. 

225 

226 Implemented like has_key. 

227 

228 Examples 

229 -------- 

230 >>> s = Struct(a=10) 

231 >>> s.hasattr('a') 

232 True 

233 >>> s.hasattr('b') 

234 False 

235 >>> s.hasattr('get') 

236 False 

237 """ 

238 return key in self 

239 

240 def allow_new_attr(self, allow=True): 

241 """Set whether new attributes can be created in this Struct. 

242 

243 This can be used to catch typos by verifying that the attribute user 

244 tries to change already exists in this Struct. 

245 """ 

246 object.__setattr__(self, "_allownew", allow) 

247 

248 def merge(self, __loc_data__=None, __conflict_solve=None, **kw): 

249 """Merge two Structs with customizable conflict resolution. 

250 

251 This is similar to :meth:`update`, but much more flexible. First, a 

252 dict is made from data+key=value pairs. When merging this dict with 

253 the Struct S, the optional dictionary 'conflict' is used to decide 

254 what to do. 

255 

256 If conflict is not given, the default behavior is to preserve any keys 

257 with their current value (the opposite of the :meth:`update` method's 

258 behavior). 

259 

260 Parameters 

261 ---------- 

262 __loc_data__ : dict, Struct 

263 The data to merge into self 

264 __conflict_solve : dict 

265 The conflict policy dict. The keys are binary functions used to 

266 resolve the conflict and the values are lists of strings naming 

267 the keys the conflict resolution function applies to. Instead of 

268 a list of strings a space separated string can be used, like 

269 'a b c'. 

270 **kw : dict 

271 Additional key, value pairs to merge in 

272 

273 Notes 

274 ----- 

275 The `__conflict_solve` dict is a dictionary of binary functions which will be used to 

276 solve key conflicts. Here is an example:: 

277 

278 __conflict_solve = dict( 

279 func1=['a','b','c'], 

280 func2=['d','e'] 

281 ) 

282 

283 In this case, the function :func:`func1` will be used to resolve 

284 keys 'a', 'b' and 'c' and the function :func:`func2` will be used for 

285 keys 'd' and 'e'. This could also be written as:: 

286 

287 __conflict_solve = dict(func1='a b c',func2='d e') 

288 

289 These functions will be called for each key they apply to with the 

290 form:: 

291 

292 func1(self['a'], other['a']) 

293 

294 The return value is used as the final merged value. 

295 

296 As a convenience, merge() provides five (the most commonly needed) 

297 pre-defined policies: preserve, update, add, add_flip and add_s. The 

298 easiest explanation is their implementation:: 

299 

300 preserve = lambda old,new: old 

301 update = lambda old,new: new 

302 add = lambda old,new: old + new 

303 add_flip = lambda old,new: new + old # note change of order! 

304 add_s = lambda old,new: old + ' ' + new # only for str! 

305 

306 You can use those four words (as strings) as keys instead 

307 of defining them as functions, and the merge method will substitute 

308 the appropriate functions for you. 

309 

310 For more complicated conflict resolution policies, you still need to 

311 construct your own functions. 

312 

313 Examples 

314 -------- 

315 This show the default policy: 

316 

317 >>> s = Struct(a=10,b=30) 

318 >>> s2 = Struct(a=20,c=40) 

319 >>> s.merge(s2) 

320 >>> sorted(s.items()) 

321 [('a', 10), ('b', 30), ('c', 40)] 

322 

323 Now, show how to specify a conflict dict: 

324 

325 >>> s = Struct(a=10,b=30) 

326 >>> s2 = Struct(a=20,b=40) 

327 >>> conflict = {'update':'a','add':'b'} 

328 >>> s.merge(s2,conflict) 

329 >>> sorted(s.items()) 

330 [('a', 20), ('b', 70)] 

331 """ 

332 

333 data_dict = dict(__loc_data__, **kw) 

334 

335 # policies for conflict resolution: two argument functions which return 

336 # the value that will go in the new struct 

337 preserve = lambda old, new: old 

338 update = lambda old, new: new 

339 add = lambda old, new: old + new 

340 add_flip = lambda old, new: new + old # note change of order! 

341 add_s = lambda old, new: old + " " + new 

342 

343 # default policy is to keep current keys when there's a conflict 

344 conflict_solve = dict.fromkeys(self, preserve) 

345 

346 # the confli_allownewct_solve dictionary is given by the user 'inverted': we 

347 # need a name-function mapping, it comes as a function -> names 

348 # dict. Make a local copy (b/c we'll make changes), replace user 

349 # strings for the three builtin policies and invert it. 

350 if __conflict_solve: 

351 inv_conflict_solve_user = __conflict_solve.copy() 

352 for name, func in [ 

353 ("preserve", preserve), 

354 ("update", update), 

355 ("add", add), 

356 ("add_flip", add_flip), 

357 ("add_s", add_s), 

358 ]: 

359 if name in inv_conflict_solve_user: 

360 inv_conflict_solve_user[func] = inv_conflict_solve_user[name] 

361 del inv_conflict_solve_user[name] 

362 conflict_solve.update(self.__dict_invert(inv_conflict_solve_user)) 

363 for key in data_dict: 

364 if key not in self: 

365 self[key] = data_dict[key] 

366 else: 

367 self[key] = conflict_solve[key](self[key], data_dict[key])