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
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
1"""A dict subclass that supports attribute style access.
3Can probably be replaced by types.SimpleNamespace from Python 3.3
4"""
6from __future__ import annotations
8from typing import Any, Dict
10__all__ = ["Struct"]
13class Struct(Dict[Any, Any]):
14 """A dict subclass with attribute style access.
16 This dict subclass has a a few extra features:
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 """
26 _allownew = True
28 def __init__(self, *args, **kw):
29 """Initialize with a dictionary, another Struct, or data.
31 Parameters
32 ----------
33 *args : dict, Struct
34 Initialize with one dict or Struct
35 **kw : dict
36 Initialize with key, value pairs.
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)
52 def __setitem__(self, key, value):
53 """Set an item with check for allownew.
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)
74 def __setattr__(self, key, value):
75 """Set an attr with protection of class members.
77 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to
78 :exc:`AttributeError`.
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
106 def __getattr__(self, key):
107 """Get an attr by calling :meth:`dict.__getitem__`.
109 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to
110 :exc:`AttributeError`.
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
133 def __iadd__(self, other):
134 """s += s2 is a shorthand for s.merge(s2).
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
147 def __add__(self, other):
148 """s + s2 -> New Struct made from s.merge(s2).
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
162 def __sub__(self, other):
163 """s1 - s2 -> remove keys in s2 from s1.
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
177 def __isub__(self, other):
178 """Inplace remove keys from self that are in other.
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
193 def __dict_invert(self, data):
194 """Helper function for merge.
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
207 def dict(self):
208 """Get the dict representation of the struct."""
209 return self
211 def copy(self):
212 """Return a copy as a Struct.
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))
223 def hasattr(self, key):
224 """hasattr function available as a method.
226 Implemented like has_key.
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
240 def allow_new_attr(self, allow=True):
241 """Set whether new attributes can be created in this Struct.
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)
248 def merge(self, __loc_data__=None, __conflict_solve=None, **kw):
249 """Merge two Structs with customizable conflict resolution.
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.
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).
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
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::
278 __conflict_solve = dict(
279 func1=['a','b','c'],
280 func2=['d','e']
281 )
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::
287 __conflict_solve = dict(func1='a b c',func2='d e')
289 These functions will be called for each key they apply to with the
290 form::
292 func1(self['a'], other['a'])
294 The return value is used as the final merged value.
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::
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!
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.
310 For more complicated conflict resolution policies, you still need to
311 construct your own functions.
313 Examples
314 --------
315 This show the default policy:
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)]
323 Now, show how to specify a conflict dict:
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 """
333 data_dict = dict(__loc_data__, **kw)
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
343 # default policy is to keep current keys when there's a conflict
344 conflict_solve = dict.fromkeys(self, preserve)
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])