ipstruct.py
400 lines
| 11.9 KiB
| text/x-python
|
PythonLexer
Brian Granger
|
r2078 | #!/usr/bin/env python | ||
# encoding: utf-8 | ||||
"""A dict subclass that supports attribute style access. | ||||
Authors: | ||||
* Fernando Perez (original) | ||||
* Brian Granger (refactoring to a dict subclass) | ||||
Fernando Perez
|
r1853 | """ | ||
fperez
|
r0 | |||
Brian Granger
|
r2078 | #----------------------------------------------------------------------------- | ||
# Copyright (C) 2008-2009 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 | |||
vivainio
|
r464 | import pprint | ||
fperez
|
r52 | |||
Brian Granger
|
r2023 | from IPython.utils.genutils import list2dict2 | ||
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. | ||||
""" | ||||
fperez
|
r0 | |||
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 | ||
---------- | ||||
Brian Granger
|
r2078 | args : dict, Struct | ||
Initialize with one dict or Struct | ||||
Brian Granger
|
r2077 | kw : dict | ||
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) | ||||
>>> s2.keys() | ||||
['a', 'c', 'b'] | ||||
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: | ||||
... print 'this is not allowed' | ||||
... | ||||
this is not allowed | ||||
""" | ||||
if not self._allownew and not self.has_key(key): | ||||
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 | ||||
Brian Granger
|
r2077 | 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): | ||||
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) | ||||
except KeyError, e: | ||||
raise AttributeError(e) | ||||
Brian Granger
|
r2077 | |||
def __getattr__(self, key): | ||||
Brian Granger
|
r2078 | """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) | ||||
<type 'builtin_function_or_method'> | ||||
>>> try: | ||||
... s.b | ||||
... except AttributeError: | ||||
... print "I don't have that key" | ||||
... | ||||
I don't have that key | ||||
""" | ||||
Brian Granger
|
r2077 | try: | ||
Brian Granger
|
r2078 | result = self[key] | ||
Brian Granger
|
r2077 | except KeyError: | ||
raise AttributeError(key) | ||||
else: | ||||
return result | ||||
def __iadd__(self, other): | ||||
Brian Granger
|
r2078 | """s += s2 is a shorthand for s.merge(s2). | ||
Examples | ||||
-------- | ||||
>>> s = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=20,c=40) | ||||
>>> s += s2 | ||||
>>> s | ||||
{'a': 10, 'c': 40, 'b': 30} | ||||
""" | ||||
fperez
|
r0 | self.merge(other) | ||
return self | ||||
def __add__(self,other): | ||||
Brian Granger
|
r2078 | """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 | ||||
>>> s | ||||
{'a': 10, 'c': 40, 'b': 30} | ||||
""" | ||||
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. | ||
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. | ||
Examples | ||||
-------- | ||||
>>> s1 = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=40) | ||||
>>> s1 -= s2 | ||||
>>> s1 | ||||
{'b': 30} | ||||
""" | ||||
fperez
|
r0 | for k in other.keys(): | ||
if self.has_key(k): | ||||
Brian Granger
|
r2078 | del self[k] | ||
return self | ||||
fperez
|
r0 | |||
Brian Granger
|
r2077 | def __dict_invert(self, data): | ||
"""Helper function for merge. | ||||
Brian Granger
|
r2078 | |||
Brian Granger
|
r2077 | 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. | ||||
""" | ||||
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. | ||||
Brian Granger
|
r2077 | |||
Brian Granger
|
r2078 | Examples | ||
-------- | ||||
Brian Granger
|
r2077 | |||
Brian Granger
|
r2078 | >>> s = Struct(a=10,b=30) | ||
>>> s2 = s.copy() | ||||
>>> s2 | ||||
{'a': 10, 'b': 30} | ||||
>>> type(s2).__name__ | ||||
'Struct' | ||||
""" | ||||
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 | """ | ||
Brian Granger
|
r2078 | return self.has_key(key) | ||
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 | ||||
---------- | ||||
__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. | ||||
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: | ||||
>>> s = Struct(a=10,b=30) | ||||
>>> s2 = Struct(a=20,c=40) | ||||
>>> s.merge(s2) | ||||
>>> s | ||||
{'a': 10, 'c': 40, 'b': 30} | ||||
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) | ||||
>>> s | ||||
{'a': 20, 'b': 70} | ||||
""" | ||||
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 | ||
Brian Granger
|
r2077 | conflict_solve = list2dict2(self.keys(), default = 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
|
r0 | #print 'merge. conflict_solve: '; pprint(conflict_solve) # dbg | ||
#print '*'*50,'in merger. conflict_solver:'; pprint(conflict_solve) | ||||
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]) | ||||