1# encoding: utf-8
2"""A dict subclass that supports attribute style access.
3
4Authors:
5
6* Fernando Perez (original)
7* Brian Granger (refactoring to a dict subclass)
8"""
9from typing import Any
10
11#-----------------------------------------------------------------------------
12# Copyright (C) 2008-2011 The IPython Development Team
13#
14# Distributed under the terms of the BSD License. The full license is in
15# the file COPYING, distributed as part of this software.
16#-----------------------------------------------------------------------------
17
18#-----------------------------------------------------------------------------
19# Imports
20#-----------------------------------------------------------------------------
21
22__all__ = ['Struct']
23
24#-----------------------------------------------------------------------------
25# Code
26#-----------------------------------------------------------------------------
27
28
29class Struct(dict):
30 """A dict subclass with attribute style access.
31
32 This dict subclass has a a few extra features:
33
34 * Attribute style access.
35 * Protection of class members (like keys, items) when using attribute
36 style access.
37 * The ability to restrict assignment to only existing keys.
38 * Intelligent merging.
39 * Overloaded operators.
40 """
41 _allownew = True
42 def __init__(self, *args, **kw):
43 """Initialize with a dictionary, another Struct, or data.
44
45 Parameters
46 ----------
47 *args : dict, Struct
48 Initialize with one dict or Struct
49 **kw : dict
50 Initialize with key, value pairs.
51
52 Examples
53 --------
54 >>> s = Struct(a=10,b=30)
55 >>> s.a
56 10
57 >>> s.b
58 30
59 >>> s2 = Struct(s,c=30)
60 >>> sorted(s2.keys())
61 ['a', 'b', 'c']
62 """
63 object.__setattr__(self, '_allownew', True)
64 dict.__init__(self, *args, **kw)
65
66 def __setitem__(self, key: str, value: Any):
67 """Set an item with check for allownew.
68
69 Examples
70 --------
71 >>> s = Struct()
72 >>> s['a'] = 10
73 >>> s.allow_new_attr(False)
74 >>> s['a'] = 10
75 >>> s['a']
76 10
77 >>> try:
78 ... s['b'] = 20
79 ... except KeyError:
80 ... print('this is not allowed')
81 ...
82 this is not allowed
83 """
84 if not self._allownew and key not in self:
85 raise KeyError(
86 "can't create new attribute %s when allow_new_attr(False)" % key)
87 dict.__setitem__(self, key, value)
88
89 def __setattr__(self, key: str, value: Any):
90 """Set an attr with protection of class members.
91
92 This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to
93 :exc:`AttributeError`.
94
95 Examples
96 --------
97 >>> s = Struct()
98 >>> s.a = 10
99 >>> s.a
100 10
101 >>> try:
102 ... s.get = 10
103 ... except AttributeError:
104 ... print("you can't set a class member")
105 ...
106 you can't set a class member
107 """
108 # If key is an str it might be a class member or instance var
109 if isinstance(key, str):
110 # I can't simply call hasattr here because it calls getattr, which
111 # calls self.__getattr__, which returns True for keys in
112 # self._data. But I only want keys in the class and in
113 # self.__dict__
114 if key in self.__dict__ or hasattr(Struct, key):
115 raise AttributeError(
116 'attr %s is a protected member of class Struct.' % key
117 )
118 try:
119 self.__setitem__(key, value)
120 except KeyError as e:
121 raise AttributeError(e) from e
122
123 def __getattr__(self, key: str) -> Any:
124 """Get an attr by calling :meth:`dict.__getitem__`.
125
126 Like :meth:`__setattr__`, this method converts :exc:`KeyError` to
127 :exc:`AttributeError`.
128
129 Examples
130 --------
131 >>> s = Struct(a=10)
132 >>> s.a
133 10
134 >>> type(s.get)
135 <...method'>
136 >>> try:
137 ... s.b
138 ... except AttributeError:
139 ... print("I don't have that key")
140 ...
141 I don't have that key
142 """
143 try:
144 result = self[key]
145 except KeyError as e:
146 raise AttributeError(key) from e
147 else:
148 return result
149
150 def __iadd__(self, other):
151 """s += s2 is a shorthand for s.merge(s2).
152
153 Examples
154 --------
155 >>> s = Struct(a=10,b=30)
156 >>> s2 = Struct(a=20,c=40)
157 >>> s += s2
158 >>> sorted(s.keys())
159 ['a', 'b', 'c']
160 """
161 self.merge(other)
162 return self
163
164 def __add__(self,other):
165 """s + s2 -> New Struct made from s.merge(s2).
166
167 Examples
168 --------
169 >>> s1 = Struct(a=10,b=30)
170 >>> s2 = Struct(a=20,c=40)
171 >>> s = s1 + s2
172 >>> sorted(s.keys())
173 ['a', 'b', 'c']
174 """
175 sout = self.copy()
176 sout.merge(other)
177 return sout
178
179 def __sub__(self,other):
180 """s1 - s2 -> remove keys in s2 from s1.
181
182 Examples
183 --------
184 >>> s1 = Struct(a=10,b=30)
185 >>> s2 = Struct(a=40)
186 >>> s = s1 - s2
187 >>> s
188 {'b': 30}
189 """
190 sout = self.copy()
191 sout -= other
192 return sout
193
194 def __isub__(self,other):
195 """Inplace remove keys from self that are in other.
196
197 Examples
198 --------
199 >>> s1 = Struct(a=10,b=30)
200 >>> s2 = Struct(a=40)
201 >>> s1 -= s2
202 >>> s1
203 {'b': 30}
204 """
205 for k in other.keys():
206 if k in self:
207 del self[k]
208 return self
209
210 def __dict_invert(self, data):
211 """Helper function for merge.
212
213 Takes a dictionary whose values are lists and returns a dict with
214 the elements of each list as keys and the original keys as values.
215 """
216 outdict = {}
217 for k,lst in data.items():
218 if isinstance(lst, str):
219 lst = lst.split()
220 for entry in lst:
221 outdict[entry] = k
222 return outdict
223
224 def dict(self):
225 return self
226
227 def copy(self):
228 """Return a copy as a Struct.
229
230 Examples
231 --------
232 >>> s = Struct(a=10,b=30)
233 >>> s2 = s.copy()
234 >>> type(s2) is Struct
235 True
236 """
237 return Struct(dict.copy(self))
238
239 def hasattr(self, key):
240 """hasattr function available as a method.
241
242 Implemented like has_key.
243
244 Examples
245 --------
246 >>> s = Struct(a=10)
247 >>> s.hasattr('a')
248 True
249 >>> s.hasattr('b')
250 False
251 >>> s.hasattr('get')
252 False
253 """
254 return key in self
255
256 def allow_new_attr(self, allow = True):
257 """Set whether new attributes can be created in this Struct.
258
259 This can be used to catch typos by verifying that the attribute user
260 tries to change already exists in this Struct.
261 """
262 object.__setattr__(self, '_allownew', allow)
263
264 def merge(self, __loc_data__=None, __conflict_solve=None, **kw):
265 """Merge two Structs with customizable conflict resolution.
266
267 This is similar to :meth:`update`, but much more flexible. First, a
268 dict is made from data+key=value pairs. When merging this dict with
269 the Struct S, the optional dictionary 'conflict' is used to decide
270 what to do.
271
272 If conflict is not given, the default behavior is to preserve any keys
273 with their current value (the opposite of the :meth:`update` method's
274 behavior).
275
276 Parameters
277 ----------
278 __loc_data__ : dict, Struct
279 The data to merge into self
280 __conflict_solve : dict
281 The conflict policy dict. The keys are binary functions used to
282 resolve the conflict and the values are lists of strings naming
283 the keys the conflict resolution function applies to. Instead of
284 a list of strings a space separated string can be used, like
285 'a b c'.
286 **kw : dict
287 Additional key, value pairs to merge in
288
289 Notes
290 -----
291 The `__conflict_solve` dict is a dictionary of binary functions which will be used to
292 solve key conflicts. Here is an example::
293
294 __conflict_solve = dict(
295 func1=['a','b','c'],
296 func2=['d','e']
297 )
298
299 In this case, the function :func:`func1` will be used to resolve
300 keys 'a', 'b' and 'c' and the function :func:`func2` will be used for
301 keys 'd' and 'e'. This could also be written as::
302
303 __conflict_solve = dict(func1='a b c',func2='d e')
304
305 These functions will be called for each key they apply to with the
306 form::
307
308 func1(self['a'], other['a'])
309
310 The return value is used as the final merged value.
311
312 As a convenience, merge() provides five (the most commonly needed)
313 pre-defined policies: preserve, update, add, add_flip and add_s. The
314 easiest explanation is their implementation::
315
316 preserve = lambda old,new: old
317 update = lambda old,new: new
318 add = lambda old,new: old + new
319 add_flip = lambda old,new: new + old # note change of order!
320 add_s = lambda old,new: old + ' ' + new # only for str!
321
322 You can use those four words (as strings) as keys instead
323 of defining them as functions, and the merge method will substitute
324 the appropriate functions for you.
325
326 For more complicated conflict resolution policies, you still need to
327 construct your own functions.
328
329 Examples
330 --------
331 This show the default policy:
332
333 >>> s = Struct(a=10,b=30)
334 >>> s2 = Struct(a=20,c=40)
335 >>> s.merge(s2)
336 >>> sorted(s.items())
337 [('a', 10), ('b', 30), ('c', 40)]
338
339 Now, show how to specify a conflict dict:
340
341 >>> s = Struct(a=10,b=30)
342 >>> s2 = Struct(a=20,b=40)
343 >>> conflict = {'update':'a','add':'b'}
344 >>> s.merge(s2,conflict)
345 >>> sorted(s.items())
346 [('a', 20), ('b', 70)]
347 """
348
349 data_dict = dict(__loc_data__,**kw)
350
351 # policies for conflict resolution: two argument functions which return
352 # the value that will go in the new struct
353 preserve = lambda old,new: old
354 update = lambda old,new: new
355 add = lambda old,new: old + new
356 add_flip = lambda old,new: new + old # note change of order!
357 add_s = lambda old,new: old + ' ' + new
358
359 # default policy is to keep current keys when there's a conflict
360 conflict_solve = dict.fromkeys(self, preserve)
361
362 # the conflict_solve dictionary is given by the user 'inverted': we
363 # need a name-function mapping, it comes as a function -> names
364 # dict. Make a local copy (b/c we'll make changes), replace user
365 # strings for the three builtin policies and invert it.
366 if __conflict_solve:
367 inv_conflict_solve_user = __conflict_solve.copy()
368 for name, func in [('preserve',preserve), ('update',update),
369 ('add',add), ('add_flip',add_flip),
370 ('add_s',add_s)]:
371 if name in inv_conflict_solve_user.keys():
372 inv_conflict_solve_user[func] = inv_conflict_solve_user[name]
373 del inv_conflict_solve_user[name]
374 conflict_solve.update(self.__dict_invert(inv_conflict_solve_user))
375 for key in data_dict:
376 if key not in self:
377 self[key] = data_dict[key]
378 else:
379 self[key] = conflict_solve[key](self[key],data_dict[key])
380