ipstruct.py
379 lines
| 11.6 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2078 | # encoding: utf-8 | ||
"""A dict subclass that supports attribute style access. | ||||
Bernardo B. Marques
|
r4872 | Authors: | ||
Brian Granger
|
r2078 | |||
* Fernando Perez (original) | ||||
* Brian Granger (refactoring to a dict subclass) | ||||
Fernando Perez
|
r1853 | """ | ||
fperez
|
r0 | |||
Brian Granger
|
r2078 | #----------------------------------------------------------------------------- | ||
Matthias BUSSONNIER
|
r5390 | # Copyright (C) 2008-2011 The IPython Development Team | ||
fperez
|
r0 | # | ||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
Brian Granger
|
r2078 | #----------------------------------------------------------------------------- | ||
fperez
|
r0 | |||
Brian Granger
|
r2078 | #----------------------------------------------------------------------------- | ||
# Imports | ||||
#----------------------------------------------------------------------------- | ||||
fperez
|
r0 | |||
Brian Granger
|
r2078 | __all__ = ['Struct'] | ||
#----------------------------------------------------------------------------- | ||||
# Code | ||||
#----------------------------------------------------------------------------- | ||||
class Struct(dict): | ||||
"""A dict subclass with attribute style access. | ||||
This dict subclass has a a few extra features: | ||||
fperez
|
r0 | |||
Brian Granger
|
r2078 | * 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. | ||||
""" | ||||
Brian Granger
|
r2184 | _allownew = True | ||
Brian Granger
|
r2078 | def __init__(self, *args, **kw): | ||
Brian Granger
|
r2077 | """Initialize with a dictionary, another Struct, or data. | ||
fperez
|
r0 | |||
Brian Granger
|
r2077 | Parameters | ||
---------- | ||||
Matthias Bussonnier
|
r26419 | *args : dict, Struct | ||
Brian Granger
|
r2078 | Initialize with one dict or Struct | ||
Matthias Bussonnier
|
r26419 | **kw : dict | ||
Brian Granger
|
r2077 | Initialize with key, value pairs. | ||
Examples | ||||
-------- | ||||
Brian Granger
|
r2078 | >>> s = Struct(a=10,b=30) | ||
>>> s.a | ||||
10 | ||||
>>> s.b | ||||
30 | ||||
>>> s2 = Struct(s,c=30) | ||||
Thomas Kluyver
|
r7012 | >>> sorted(s2.keys()) | ||
['a', 'b', 'c'] | ||||
fperez
|
r0 | """ | ||
Brian Granger
|
r2077 | object.__setattr__(self, '_allownew', True) | ||
Brian Granger
|
r2078 | dict.__init__(self, *args, **kw) | ||
fperez
|
r0 | |||
Brian Granger
|
r2077 | def __setitem__(self, key, value): | ||
Brian Granger
|
r2078 | """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: | ||||
Thomas Kluyver
|
r13392 | ... print('this is not allowed') | ||
Bernardo B. Marques
|
r4872 | ... | ||
Brian Granger
|
r2078 | this is not allowed | ||
""" | ||||
Bradley M. Froehle
|
r7859 | if not self._allownew and key not in self: | ||
Brian Granger
|
r2078 | 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. | ||||
Bernardo B. Marques
|
r4872 | This calls :meth:`self.__setitem__` but convert :exc:`KeyError` to | ||
Brian Granger
|
r2078 | :exc:`AttributeError`. | ||
Examples | ||||
-------- | ||||
>>> s = Struct() | ||||
>>> s.a = 10 | ||||
>>> s.a | ||||
10 | ||||
>>> try: | ||||
... s.get = 10 | ||||
... except AttributeError: | ||||
Thomas Kluyver
|
r13392 | ... print("you can't set a class member") | ||
Bernardo B. Marques
|
r4872 | ... | ||
Brian Granger
|
r2078 | you can't set a class member | ||
""" | ||||
# If key is an str it might be a class member or instance var | ||||
Brian Granger
|
r2077 | if isinstance(key, str): | ||
# I can't simply call hasattr here because it calls getattr, which | ||||
Bernardo B. Marques
|
r4872 | # calls self.__getattr__, which returns True for keys in | ||
Brian Granger
|
r2077 | # self._data. But I only want keys in the class and in | ||
# self.__dict__ | ||||
if key in self.__dict__ or hasattr(Struct, key): | ||||
Brian Granger
|
r2078 | raise AttributeError( | ||
'attr %s is a protected member of class Struct.' % key | ||||
Brian Granger
|
r2077 | ) | ||
Brian Granger
|
r2078 | try: | ||
self.__setitem__(key, value) | ||||
Matthias BUSSONNIER
|
r7787 | except KeyError as e: | ||
Ram Rachum
|
r25833 | raise AttributeError(e) from e | ||
Brian Granger
|
r2077 | |||
def __getattr__(self, key): | ||||
Brian Granger
|
r2078 | """Get an attr by calling :meth:`dict.__getitem__`. | ||
Bernardo B. Marques
|
r4872 | Like :meth:`__setattr__`, this method converts :exc:`KeyError` to | ||
Brian Granger
|
r2078 | :exc:`AttributeError`. | ||
Examples | ||||
-------- | ||||
>>> s = Struct(a=10) | ||||
>>> s.a | ||||
10 | ||||
>>> type(s.get) | ||||
Nikita Kniazev
|
r27235 | <...method'> | ||
Brian Granger
|
r2078 | >>> try: | ||
... s.b | ||||
... except AttributeError: | ||||
Thomas Kluyver
|
r13392 | ... print("I don't have that key") | ||
Bernardo B. Marques
|
r4872 | ... | ||
Brian Granger
|
r2078 | I don't have that key | ||
""" | ||||
Brian Granger
|
r2077 | try: | ||
Brian Granger
|
r2078 | result = self[key] | ||
Ram Rachum
|
r25833 | except KeyError as e: | ||
raise AttributeError(key) from e | ||||
Brian Granger
|
r2077 | else: | ||
return result | ||||
def __iadd__(self, other): | ||||
Brian Granger
|
r2078 | """s += s2 is a shorthand for s.merge(s2). | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
>>> s = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=20,c=40) | ||||
>>> s += s2 | ||||
Thomas Kluyver
|
r7012 | >>> sorted(s.keys()) | ||
['a', 'b', 'c'] | ||||
Brian Granger
|
r2078 | """ | ||
fperez
|
r0 | self.merge(other) | ||
return self | ||||
def __add__(self,other): | ||||
Brian Granger
|
r2078 | """s + s2 -> New Struct made from s.merge(s2). | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
>>> s1 = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=20,c=40) | ||||
>>> s = s1 + s2 | ||||
Thomas Kluyver
|
r7012 | >>> sorted(s.keys()) | ||
['a', 'b', 'c'] | ||||
Brian Granger
|
r2078 | """ | ||
sout = self.copy() | ||||
sout.merge(other) | ||||
return sout | ||||
fperez
|
r0 | |||
def __sub__(self,other): | ||||
Brian Granger
|
r2078 | """s1 - s2 -> remove keys in s2 from s1. | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
>>> s1 = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=40) | ||||
>>> s = s1 - s2 | ||||
>>> s | ||||
{'b': 30} | ||||
""" | ||||
sout = self.copy() | ||||
sout -= other | ||||
return sout | ||||
fperez
|
r0 | |||
def __isub__(self,other): | ||||
Brian Granger
|
r2078 | """Inplace remove keys from self that are in other. | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
>>> s1 = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=40) | ||||
>>> s1 -= s2 | ||||
>>> s1 | ||||
{'b': 30} | ||||
""" | ||||
fperez
|
r0 | for k in other.keys(): | ||
Bradley M. Froehle
|
r7859 | if k in self: | ||
Brian Granger
|
r2078 | del self[k] | ||
return self | ||||
fperez
|
r0 | |||
Brian Granger
|
r2077 | def __dict_invert(self, data): | ||
Bernardo B. Marques
|
r4872 | """Helper function for merge. | ||
Brian Granger
|
r2078 | |||
Bernardo B. Marques
|
r4872 | Takes a dictionary whose values are lists and returns a dict with | ||
Brian Granger
|
r2077 | the elements of each list as keys and the original keys as values. | ||
""" | ||||
fperez
|
r0 | outdict = {} | ||
Brian Granger
|
r2077 | for k,lst in data.items(): | ||
if isinstance(lst, str): | ||||
fperez
|
r0 | lst = lst.split() | ||
for entry in lst: | ||||
outdict[entry] = k | ||||
return outdict | ||||
Brian Granger
|
r2078 | |||
fperez
|
r0 | def dict(self): | ||
Brian Granger
|
r2078 | return self | ||
def copy(self): | ||||
"""Return a copy as a Struct. | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
>>> s = Struct(a=10,b=30) | ||||
>>> s2 = s.copy() | ||||
Thomas Kluyver
|
r7012 | >>> type(s2) is Struct | ||
True | ||||
Brian Granger
|
r2078 | """ | ||
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 | ||||
Brian Granger
|
r2077 | """ | ||
Bradley M. Froehle
|
r7859 | return key in self | ||
Brian Granger
|
r2078 | |||
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) | ||||
Brian Granger
|
r2077 | def merge(self, __loc_data__=None, __conflict_solve=None, **kw): | ||
Brian Granger
|
r2078 | """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. | ||||
fperez
|
r0 | If conflict is not given, the default behavior is to preserve any keys | ||
Brian Granger
|
r2078 | with their current value (the opposite of the :meth:`update` method's | ||
fperez
|
r0 | behavior). | ||
Brian Granger
|
r2078 | |||
Parameters | ||||
---------- | ||||
Matthias Bussonnier
|
r26419 | __loc_data__ : dict, Struct | ||
Brian Granger
|
r2078 | 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'. | ||||
Matthias Bussonnier
|
r26419 | **kw : dict | ||
Brian Granger
|
r2078 | 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:: | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | __conflict_solve = dict( | ||
func1=['a','b','c'], | ||||
func2=['d','e'] | ||||
) | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | In this case, the function :func:`func1` will be used to resolve | ||
Bernardo B. Marques
|
r4872 | keys 'a', 'b' and 'c' and the function :func:`func2` will be used for | ||
Brian Granger
|
r2078 | 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. | ||||
fperez
|
r0 | As a convenience, merge() provides five (the most commonly needed) | ||
pre-defined policies: preserve, update, add, add_flip and add_s. The | ||||
Brian Granger
|
r2078 | 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 | ||||
fperez
|
r0 | of defining them as functions, and the merge method will substitute | ||
Brian Granger
|
r2078 | the appropriate functions for you. | ||
fperez
|
r0 | For more complicated conflict resolution policies, you still need to | ||
Brian Granger
|
r2078 | construct your own functions. | ||
Examples | ||||
-------- | ||||
This show the default policy: | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | >>> s = Struct(a=10,b=30) | ||
>>> s2 = Struct(a=20,c=40) | ||||
>>> s.merge(s2) | ||||
Thomas Kluyver
|
r7012 | >>> sorted(s.items()) | ||
[('a', 10), ('b', 30), ('c', 40)] | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | Now, show how to specify a conflict dict: | ||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r2078 | >>> s = Struct(a=10,b=30) | ||
>>> s2 = Struct(a=20,b=40) | ||||
>>> conflict = {'update':'a','add':'b'} | ||||
>>> s.merge(s2,conflict) | ||||
Thomas Kluyver
|
r7012 | >>> sorted(s.items()) | ||
[('a', 20), ('b', 70)] | ||||
Brian Granger
|
r2078 | """ | ||
data_dict = dict(__loc_data__,**kw) | ||||
fperez
|
r0 | # 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 | ||||
Brian Granger
|
r2078 | |||
fperez
|
r0 | # default policy is to keep current keys when there's a conflict | ||
Thomas Kluyver
|
r9471 | conflict_solve = dict.fromkeys(self, preserve) | ||
Brian Granger
|
r2078 | |||
fperez
|
r0 | # 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), | ||||
Fernando Perez
|
r1280 | ('add',add), ('add_flip',add_flip), | ||
('add_s',add_s)]: | ||||
fperez
|
r0 | if name in inv_conflict_solve_user.keys(): | ||
inv_conflict_solve_user[func] = inv_conflict_solve_user[name] | ||||
del inv_conflict_solve_user[name] | ||||
Brian Granger
|
r2077 | conflict_solve.update(self.__dict_invert(inv_conflict_solve_user)) | ||
fperez
|
r5 | for key in data_dict: | ||
fperez
|
r0 | if key not in self: | ||
self[key] = data_dict[key] | ||||
else: | ||||
self[key] = conflict_solve[key](self[key],data_dict[key]) | ||||