_struct.py 11 KB

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