ipstruct.py 12 KB

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