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