ipstruct.py 11 KB

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