You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
12 KiB
Python
380 lines
12 KiB
Python
2 years ago
|
# encoding: utf-8
|
||
|
"""A dict subclass that supports attribute style access.
|
||
|
|
||
|
Authors:
|
||
|
|
||
|
* Fernando Perez (original)
|
||
|
* Brian Granger (refactoring to a dict subclass)
|
||
|
"""
|
||
|
|
||
|
#-----------------------------------------------------------------------------
|
||
|
# Copyright (C) 2008-2011 The IPython Development Team
|
||
|
#
|
||
|
# Distributed under the terms of the BSD License. The full license is in
|
||
|
# the file COPYING, distributed as part of this software.
|
||
|
#-----------------------------------------------------------------------------
|
||
|
|
||
|
#-----------------------------------------------------------------------------
|
||
|
# Imports
|
||
|
#-----------------------------------------------------------------------------
|
||
|
|
||
|
__all__ = ['Struct']
|
||
|
|
||
|
#-----------------------------------------------------------------------------
|
||
|
# Code
|
||
|
#-----------------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
class Struct(dict):
|
||
|
"""A dict subclass with attribute style access.
|
||
|
|
||
|
This dict subclass has a a few extra features:
|
||
|
|
||
|
* Attribute style access.
|
||
|
* Protection of class members (like keys, items) when using attribute
|
||
|
style access.
|
||
|
* The ability to restrict assignment to only existing keys.
|
||
|
* Intelligent merging.
|
||
|
* Overloaded operators.
|
||
|
"""
|
||
|
_allownew = True
|
||
|
def __init__(self, *args, **kw):
|
||
|
"""Initialize with a dictionary, another Struct, or data.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
*args : dict, Struct
|
||
|
Initialize with one dict or Struct
|
||
|
**kw : dict
|
||
|
Initialize with key, value pairs.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct(a=10,b=30)
|
||
|
>>> s.a
|
||
|
10
|
||
|
>>> s.b
|
||
|
30
|
||
|
>>> s2 = Struct(s,c=30)
|
||
|
>>> sorted(s2.keys())
|
||
|
['a', 'b', 'c']
|
||
|
"""
|
||
|
object.__setattr__(self, '_allownew', True)
|
||
|
dict.__init__(self, *args, **kw)
|
||
|
|
||
|
def __setitem__(self, key, value):
|
||
|
"""Set an item with check for allownew.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct()
|
||
|
>>> s['a'] = 10
|
||
|
>>> s.allow_new_attr(False)
|
||
|
>>> s['a'] = 10
|
||
|
>>> s['a']
|
||
|
10
|
||
|
>>> try:
|
||
|
... s['b'] = 20
|
||
|
... except KeyError:
|
||
|
... print('this is not allowed')
|
||
|
...
|
||
|
this is not allowed
|
||
|
"""
|
||
|
if not self._allownew and key not in self:
|
||
|
raise KeyError(
|
||
|
"can't create new attribute %s when allow_new_attr(False)" % key)
|
||
|
dict.__setitem__(self, key, value)
|
||
|
|
||
|
def __setattr__(self, key, value):
|
||
|
"""Set an attr with protection of class members.
|
||
|
|
||
|
This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to
|
||
|
:exc:`AttributeError`.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct()
|
||
|
>>> s.a = 10
|
||
|
>>> s.a
|
||
|
10
|
||
|
>>> try:
|
||
|
... s.get = 10
|
||
|
... except AttributeError:
|
||
|
... print("you can't set a class member")
|
||
|
...
|
||
|
you can't set a class member
|
||
|
"""
|
||
|
# If key is an str it might be a class member or instance var
|
||
|
if isinstance(key, str):
|
||
|
# I can't simply call hasattr here because it calls getattr, which
|
||
|
# calls self.__getattr__, which returns True for keys in
|
||
|
# self._data. But I only want keys in the class and in
|
||
|
# self.__dict__
|
||
|
if key in self.__dict__ or hasattr(Struct, key):
|
||
|
raise AttributeError(
|
||
|
'attr %s is a protected member of class Struct.' % key
|
||
|
)
|
||
|
try:
|
||
|
self.__setitem__(key, value)
|
||
|
except KeyError as e:
|
||
|
raise AttributeError(e) from e
|
||
|
|
||
|
def __getattr__(self, key):
|
||
|
"""Get an attr by calling :meth:`dict.__getitem__`.
|
||
|
|
||
|
Like :meth:`__setattr__`, this method converts :exc:`KeyError` to
|
||
|
:exc:`AttributeError`.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct(a=10)
|
||
|
>>> s.a
|
||
|
10
|
||
|
>>> type(s.get)
|
||
|
<...method'>
|
||
|
>>> try:
|
||
|
... s.b
|
||
|
... except AttributeError:
|
||
|
... print("I don't have that key")
|
||
|
...
|
||
|
I don't have that key
|
||
|
"""
|
||
|
try:
|
||
|
result = self[key]
|
||
|
except KeyError as e:
|
||
|
raise AttributeError(key) from e
|
||
|
else:
|
||
|
return result
|
||
|
|
||
|
def __iadd__(self, other):
|
||
|
"""s += s2 is a shorthand for s.merge(s2).
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=20,c=40)
|
||
|
>>> s += s2
|
||
|
>>> sorted(s.keys())
|
||
|
['a', 'b', 'c']
|
||
|
"""
|
||
|
self.merge(other)
|
||
|
return self
|
||
|
|
||
|
def __add__(self,other):
|
||
|
"""s + s2 -> New Struct made from s.merge(s2).
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s1 = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=20,c=40)
|
||
|
>>> s = s1 + s2
|
||
|
>>> sorted(s.keys())
|
||
|
['a', 'b', 'c']
|
||
|
"""
|
||
|
sout = self.copy()
|
||
|
sout.merge(other)
|
||
|
return sout
|
||
|
|
||
|
def __sub__(self,other):
|
||
|
"""s1 - s2 -> remove keys in s2 from s1.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s1 = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=40)
|
||
|
>>> s = s1 - s2
|
||
|
>>> s
|
||
|
{'b': 30}
|
||
|
"""
|
||
|
sout = self.copy()
|
||
|
sout -= other
|
||
|
return sout
|
||
|
|
||
|
def __isub__(self,other):
|
||
|
"""Inplace remove keys from self that are in other.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s1 = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=40)
|
||
|
>>> s1 -= s2
|
||
|
>>> s1
|
||
|
{'b': 30}
|
||
|
"""
|
||
|
for k in other.keys():
|
||
|
if k in self:
|
||
|
del self[k]
|
||
|
return self
|
||
|
|
||
|
def __dict_invert(self, data):
|
||
|
"""Helper function for merge.
|
||
|
|
||
|
Takes a dictionary whose values are lists and returns a dict with
|
||
|
the elements of each list as keys and the original keys as values.
|
||
|
"""
|
||
|
outdict = {}
|
||
|
for k,lst in data.items():
|
||
|
if isinstance(lst, str):
|
||
|
lst = lst.split()
|
||
|
for entry in lst:
|
||
|
outdict[entry] = k
|
||
|
return outdict
|
||
|
|
||
|
def dict(self):
|
||
|
return self
|
||
|
|
||
|
def copy(self):
|
||
|
"""Return a copy as a Struct.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct(a=10,b=30)
|
||
|
>>> s2 = s.copy()
|
||
|
>>> type(s2) is Struct
|
||
|
True
|
||
|
"""
|
||
|
return Struct(dict.copy(self))
|
||
|
|
||
|
def hasattr(self, key):
|
||
|
"""hasattr function available as a method.
|
||
|
|
||
|
Implemented like has_key.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> s = Struct(a=10)
|
||
|
>>> s.hasattr('a')
|
||
|
True
|
||
|
>>> s.hasattr('b')
|
||
|
False
|
||
|
>>> s.hasattr('get')
|
||
|
False
|
||
|
"""
|
||
|
return key in self
|
||
|
|
||
|
def allow_new_attr(self, allow = True):
|
||
|
"""Set whether new attributes can be created in this Struct.
|
||
|
|
||
|
This can be used to catch typos by verifying that the attribute user
|
||
|
tries to change already exists in this Struct.
|
||
|
"""
|
||
|
object.__setattr__(self, '_allownew', allow)
|
||
|
|
||
|
def merge(self, __loc_data__=None, __conflict_solve=None, **kw):
|
||
|
"""Merge two Structs with customizable conflict resolution.
|
||
|
|
||
|
This is similar to :meth:`update`, but much more flexible. First, a
|
||
|
dict is made from data+key=value pairs. When merging this dict with
|
||
|
the Struct S, the optional dictionary 'conflict' is used to decide
|
||
|
what to do.
|
||
|
|
||
|
If conflict is not given, the default behavior is to preserve any keys
|
||
|
with their current value (the opposite of the :meth:`update` method's
|
||
|
behavior).
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
__loc_data__ : dict, Struct
|
||
|
The data to merge into self
|
||
|
__conflict_solve : dict
|
||
|
The conflict policy dict. The keys are binary functions used to
|
||
|
resolve the conflict and the values are lists of strings naming
|
||
|
the keys the conflict resolution function applies to. Instead of
|
||
|
a list of strings a space separated string can be used, like
|
||
|
'a b c'.
|
||
|
**kw : dict
|
||
|
Additional key, value pairs to merge in
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
The `__conflict_solve` dict is a dictionary of binary functions which will be used to
|
||
|
solve key conflicts. Here is an example::
|
||
|
|
||
|
__conflict_solve = dict(
|
||
|
func1=['a','b','c'],
|
||
|
func2=['d','e']
|
||
|
)
|
||
|
|
||
|
In this case, the function :func:`func1` will be used to resolve
|
||
|
keys 'a', 'b' and 'c' and the function :func:`func2` will be used for
|
||
|
keys 'd' and 'e'. This could also be written as::
|
||
|
|
||
|
__conflict_solve = dict(func1='a b c',func2='d e')
|
||
|
|
||
|
These functions will be called for each key they apply to with the
|
||
|
form::
|
||
|
|
||
|
func1(self['a'], other['a'])
|
||
|
|
||
|
The return value is used as the final merged value.
|
||
|
|
||
|
As a convenience, merge() provides five (the most commonly needed)
|
||
|
pre-defined policies: preserve, update, add, add_flip and add_s. The
|
||
|
easiest explanation is their implementation::
|
||
|
|
||
|
preserve = lambda old,new: old
|
||
|
update = lambda old,new: new
|
||
|
add = lambda old,new: old + new
|
||
|
add_flip = lambda old,new: new + old # note change of order!
|
||
|
add_s = lambda old,new: old + ' ' + new # only for str!
|
||
|
|
||
|
You can use those four words (as strings) as keys instead
|
||
|
of defining them as functions, and the merge method will substitute
|
||
|
the appropriate functions for you.
|
||
|
|
||
|
For more complicated conflict resolution policies, you still need to
|
||
|
construct your own functions.
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
This show the default policy:
|
||
|
|
||
|
>>> s = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=20,c=40)
|
||
|
>>> s.merge(s2)
|
||
|
>>> sorted(s.items())
|
||
|
[('a', 10), ('b', 30), ('c', 40)]
|
||
|
|
||
|
Now, show how to specify a conflict dict:
|
||
|
|
||
|
>>> s = Struct(a=10,b=30)
|
||
|
>>> s2 = Struct(a=20,b=40)
|
||
|
>>> conflict = {'update':'a','add':'b'}
|
||
|
>>> s.merge(s2,conflict)
|
||
|
>>> sorted(s.items())
|
||
|
[('a', 20), ('b', 70)]
|
||
|
"""
|
||
|
|
||
|
data_dict = dict(__loc_data__,**kw)
|
||
|
|
||
|
# policies for conflict resolution: two argument functions which return
|
||
|
# the value that will go in the new struct
|
||
|
preserve = lambda old,new: old
|
||
|
update = lambda old,new: new
|
||
|
add = lambda old,new: old + new
|
||
|
add_flip = lambda old,new: new + old # note change of order!
|
||
|
add_s = lambda old,new: old + ' ' + new
|
||
|
|
||
|
# default policy is to keep current keys when there's a conflict
|
||
|
conflict_solve = dict.fromkeys(self, preserve)
|
||
|
|
||
|
# the conflict_solve dictionary is given by the user 'inverted': we
|
||
|
# need a name-function mapping, it comes as a function -> names
|
||
|
# dict. Make a local copy (b/c we'll make changes), replace user
|
||
|
# strings for the three builtin policies and invert it.
|
||
|
if __conflict_solve:
|
||
|
inv_conflict_solve_user = __conflict_solve.copy()
|
||
|
for name, func in [('preserve',preserve), ('update',update),
|
||
|
('add',add), ('add_flip',add_flip),
|
||
|
('add_s',add_s)]:
|
||
|
if name in inv_conflict_solve_user.keys():
|
||
|
inv_conflict_solve_user[func] = inv_conflict_solve_user[name]
|
||
|
del inv_conflict_solve_user[name]
|
||
|
conflict_solve.update(self.__dict_invert(inv_conflict_solve_user))
|
||
|
for key in data_dict:
|
||
|
if key not in self:
|
||
|
self[key] = data_dict[key]
|
||
|
else:
|
||
|
self[key] = conflict_solve[key](self[key],data_dict[key])
|
||
|
|