# DataOrdered.py
#
# Created: Jul 2016, E. Botero
# Modified: Sep 2016, E. Botero
# May 2020, E. Botero
# Jul 2020, E. Botero
# Jul 2021, E. Botero
# ----------------------------------------------------------------------
# Imports
# ----------------------------------------------------------------------
from collections import OrderedDict
# for enforcing attribute style access names
import string
chars = string.punctuation + string.whitespace
t_table = str.maketrans( chars + string.ascii_uppercase ,
'_'*len(chars) + string.ascii_lowercase )
import numpy as np
# ----------------------------------------------------------------------
# Property Class
# ----------------------------------------------------------------------
[docs]
class Property(object):
""" Used to create the root map essential to the linking in DataOrdered()
Assumptions:
N/A
Source:
N/A
"""
[docs]
def __init__(self,key=None):
""" Initializes a property
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
self._key = key
def __get__(self,obj,kls=None):
""" Gets a property
Assumptions:
N/A
Source:
N/A
Inputs:
obj
Outputs:
self.key
Properties Used:
N/A
"""
if obj is None: return self
else : return dict.__getitem__(obj,self._key)
def __set__(self,obj,val):
""" Sets a property
Assumptions:
N/A
Source:
N/A
Inputs:
obj
value
Outputs:
N/A
Properties Used:
N/A
"""
dict.__setitem__(obj,self._key,val)
def __delete__(self,obj):
""" Deletes a property
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
dict.__delitem__(obj,self._key)
# ----------------------------------------------------------------------
# DataOrdered
# ----------------------------------------------------------------------
[docs]
class DataOrdered(OrderedDict):
""" An extension of the Python dict which allows for both tag and '.' usage.
This is an ordered dictionary. So indexing it will produce deterministic results.
Assumptions:
N/A
Source:
N/A
"""
_root = Property('_root')
_map = Property('_map')
[docs]
def append(self,value,key=None):
""" Adds new values to the classes. Can also change an already appended key
Assumptions:
N/A
Source:
N/A
Inputs:
value
key
Outputs:
N/A
Properties Used:
N/A
"""
if key is None: key = value.tag
key = key.translate(t_table)
if key is None: key = value.tag
if key in self: raise KeyError('key "%s" already exists' % key)
self.__setattr__(key,value)
def __defaults__(self):
""" A stub for all classes that come later
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
pass
def __getitem__(self,k):
""" Retrieves an attribute set by a key k
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
if not (isinstance(k,int) or isinstance(k,np.int64)):
return super(DataOrdered,self).__getattribute__(k)
else:
return super(DataOrdered,self).__getattribute__(self.keys()[k])
def __new__(cls,*args,**kwarg):
""" Creates a new Data() class
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
# Make the new:
self = OrderedDict.__new__(cls)
if self.hasattr('_root'):
self._root
else:
root = [] # sentinel node
root[:] = [root, root, None]
dict.__setitem__(self,'_root',root)
dict.__setitem__(self,'_map' ,{})
# Use the base init
self.__init2()
# get base class list
klasses = self.get_bases()
# fill in defaults trunk to leaf
for klass in klasses[::-1]:
klass.__defaults__(self)
return self
[docs]
def hasattr(self,k):
try:
self.__getitem__(k)
return True
except:
return False
[docs]
def __init__(self,*args,**kwarg):
""" Initializes a new Data() class
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
# handle input data (ala class factory)
input_data = DataOrdered.__base__(*args,**kwarg)
# update this data with inputs
self.update(input_data)
def __init2(self, items=None, **kwds):
""" A helper that allows __init_ to complete the new Data() class
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
def append_value(key,value):
self[key] = value
# a dictionary
if hasattr(items, 'iterkeys'):
for key in items.keys():
append_value(key,items[key])
elif hasattr(items, 'keys'):
for key in items.keys():
append_value(key,items[key])
# items lists
elif items:
for key, value in items:
append_value(key,value)
# key words
for key, value in kwds.items():
append_value(key,value)
# iterate on values, not keys
def __iter__(self):
""" Returns all the iterable values. Can be used in a for loop.
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
return iter(self.values())
[docs]
def get_bases(self):
""" Finds the higher classes that may be built off of data
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
klasses
Properties Used:
N/A
"""
# Get the Method Resolution Order, i.e. the ancestor tree
klasses = list(self.__class__.__mro__)
# Make sure that this is a Data object, otherwise throw an error.
if DataOrdered not in klasses:
raise TypeError('class %s is not of type Data()' % self.__class__)
# Remove the last two items, dict and object. Since the line before ensures this is a data object this won't break
klasses = klasses[:-3]
return klasses
[docs]
def typestring(self):
""" This function makes the .key.key structure in string form of Data()
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
typestring = str(type(self)).split("'")[1]
typestring = typestring.split('.')
if typestring[-1] == typestring[-2]:
del typestring[-1]
typestring = '.'.join(typestring)
return typestring
[docs]
def dataname(self):
""" This function is used for printing the class
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
return "<data object '" + self.typestring() + "'>"
[docs]
def deep_set(self,keys,val):
""" Regresses through a list of keys the same value in various places in a dictionary.
Assumptions:
N/A
Source:
N/A
Inputs:
keys - The keys to iterate over
val - The value to be set
Outputs:
N/A
Properties Used:
N/A
"""
if isinstance(keys,str):
keys = keys.split('.')
data = self
if len(keys) > 1:
for k in keys[:-1]:
data = data[k]
data[ keys[-1] ] = val
return data
[docs]
def deep_get(self,keys):
""" Regresses through a list of keys to pull a specific value out
Assumptions:
N/A
Source:
N/A
Inputs:
keys - The keys to iterate over
Outputs:
value - The value to be retrieved
Properties Used:
N/A
"""
if isinstance(keys,str):
keys = keys.split('.')
data = self
if len(keys) > 1:
for k in keys[:-1]:
data = data[k]
value = data[ keys[-1] ]
return value
[docs]
def update(self,other):
""" Updates the internal values of a dictionary with given data
Assumptions:
N/A
Source:
N/A
Inputs:
other
Outputs:
N/A
Properties Used:
N/A
"""
if not isinstance(other,dict):
raise TypeError('input is not a dictionary type')
for k,v in other.items():
# recurse only if self's value is a Dict()
if k.startswith('_'):
continue
try:
self[k].update(v)
except:
self[k] = v
return
def __delattr__(self, key):
""" An override of the standard __delattr_ in Python. This deletes whatever is called by k
Assumptions:
This one tries to treat k as an object, if that fails it treats it as a key.
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
# Deleting an existing item uses self._map to find the link which is
# then removed by updating the links in the predecessor and successor nodes.
OrderedDict.__delattr__(self,key)
link_prev, link_next, key = self._map.pop(key)
link_prev[1] = link_next
link_next[0] = link_prev
def __len__(self):
""" This is overrides the Python function for checking length
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
return self.__dict__.__len__()
def __iter_basic__(self):
""" Returns all the iterable values. Can be used in a for loop.
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
root = self._root
curr = root[1]
while curr is not root:
yield curr[2]
curr = curr[1]
def __reduce__(self):
""" Reduction function used for making configs
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
items = [( k, DataOrdered.__getitem2(self,k) ) for k in DataOrdered.iterkeys(self)]
inst_dict = vars(self).copy()
for k in vars(DataOrdered()):
inst_dict.pop(k, None)
return (_reconstructor, (self.__class__,items,), inst_dict)
def __setattr__(self, key, value):
""" An override of the standard __setattr_ in Python.
Assumptions:
This one tries to treat k as an object, if that fails it treats it as a key.
Source:
N/A
Inputs:
key [key]
value [value]
Outputs:
N/A
Properties Used:
N/A
"""
# Setting a new item creates a new link which goes at the end of the linked
# list, and the inherited dictionary is updated with the new key/value pair.
if not hasattr(self,key) and not hasattr(self.__class__,key):
#if not self.has_key(key) and not hasattr(self.__class__,key):
root = dict.__getitem__(self,'_root')
last = root[0]
map = dict.__getitem__(self,'_map')
last[1] = root[0] = map[key] = [last, root, key]
OrderedDict.__setattr__(self,key, value)
def __setitem__(self,k,v):
""" An override of the standard __setattr_ in Python.
Assumptions:
This one tries to treat k as an object, if that fails it treats it as a key.
Source:
N/A
Inputs:
k [key]
v [value]
Outputs:
N/A
Properties Used:
N/A
"""
self.__setattr__(k,v)
[docs]
def clear(self):
""" Empties a dictionary
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
try:
for node in self._map.values():
del node[:]
root = self._root
root[:] = [root, root, None]
self._map.clear()
except AttributeError:
pass
self.__dict__.clear()
[docs]
def get(self,k,d=None):
""" Returns the values from k
Assumptions:
N/A
Source:
N/A
Inputs:
k
Outputs:
N/A
Properties Used:
N/A
"""
return self.__dict__.get(k,d)
[docs]
def has_key(self,k):
""" Checks if the dictionary has the key, k
Assumptions:
N/A
Source:
N/A
Inputs:
k
Outputs:
N/A
Properties Used:
N/A
"""
return k in self.__dict__
# allow override of iterators
__iter = __iter__
__getitem2 = OrderedDict.__getattribute__
[docs]
def keys(self):
""" Returns a list of keys
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
return list(self.__iter_basic__())
[docs]
def values(self):
""" Returns all values inside the Data() class.
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
values
Properties Used:
N/A
"""
return [self[key] for key in self.__iter_basic__()]
[docs]
def items(self):
""" Returns all the items inside the data class
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
values
Properties Used:
N/A
"""
return [(key, self[key]) for key in self.__iter_basic__()]
[docs]
def iterkeys(self):
""" Returns all the keys which may be iterated over
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
return self.__iter_basic__()
# for rebuilding dictionaries with attributes
def _reconstructor(klass,items):
""" For rebuilding dictionaries with attributes
Assumptions:
N/A
Source:
N/A
Inputs:
N/A
Outputs:
N/A
Properties Used:
N/A
"""
self = DataOrdered.__new__(klass)
DataOrdered.__init__(self,items)
return self