# -*- coding: utf-8 -*-
"""
core
====
Core functionality for `ahds` package
Contains:
* Python2/3 adapters
* decorator for marking deprecation
* core base classes: `Block` and `ListBlock`
"""
from __future__ import print_function
import functools as ft
import inspect
import sys
import warnings
import numpy as np
# print to stderr
_print = ft.partial(print, file=sys.stderr)
if sys.version_info[0] > 2:
# All definitions for Python3 and newer which differ from their counterparts in Python2.x
def _decode_string(data):
""" decodes binary ASCII string to python3 UTF-8 standard string """
try:
return data.decode("ASCII")
except:
return data.decode("UTF8")
# in define xrange alias which has been removed in Python3 as range now is equal to
xrange = range # xrange
# define _dict_iter_values, _dict_iter_items, _dict_iter_keys aliases imported by the
# other parts for dict.values, dict.items and dict.keys
_dict_iter_values = dict.values
_dict_iter_items = dict.items
_dict_iter_keys = dict.keys
if sys.version_info[1] >= 7:
_dict = dict
else:
from collections import OrderedDict
_dict = OrderedDict
def _qualname(o):
return o.__qualname__
_str = str
# UserList
from collections import UserList
_UserList = UserList
else:
import __builtin__
# Python2.x definitions
def _decode_string(data):
"""No decoding in Python2.x"""
return data
# try to define xrange alias pointing to the builtin xrange
try:
xrange = __builtins__['xrange']
except AttributeError:
xrange = getattr(__builtins__, 'xrange')
# define _dict_iter_values, _dict_iter_items, _dict_iter_keys aliases imported by the
# other parts for dict.itervalues, dict.iteritems and dict.iterkeys
_dict_iter_values = dict.itervalues
_dict_iter_items = dict.iteritems
_dict_iter_keys = dict.iterkeys
from collections import OrderedDict
_dict = OrderedDict
def _qualname(o):
return o.__name__
_str = __builtin__.unicode
# UserList
from UserList import UserList
_UserList = UserList
def deprecated(description):
"""Function/Class/Method decorator for warning about deprecations"""
def outer_wrapper(o):
@ft.wraps(o)
def inner_wrapper(*args, **kwargs):
if inspect.isfunction(o):
warnings.warn('function/method {} is deprecated: {}'.format(_qualname(o), description),
DeprecationWarning)
elif inspect.isclass(o):
warnings.warn('class {} is deprecated: {}'.format(_qualname(o), description), DeprecationWarning)
return o(*args, **kwargs)
return inner_wrapper
return outer_wrapper
@ft.total_ordering
class Block(object):
"""Data content block for atomic entities"""
__slots__ = ('_name', '_attrs', '_is_parent', '__dict__', '__weakref__')
def __init__(self, name):
self._name = name
self._attrs = _dict()
self._is_parent = False
@property
def name(self):
return self._name
def attrs(self):
return list(self._attrs.keys())
@property
def is_parent(self):
return self._is_parent
def add_attr(self, attr, value=None, isparent=False):
"""Add an attribute to this block object"""
try:
assert hasattr(attr, 'name') or isinstance(attr, str)
except AssertionError:
raise ValueError('attr should be str or have .name attribute')
if hasattr(attr, 'name'):
value = attr
attr = attr.name
elif not isinstance(attr, str):
raise ValueError("invalid type for attr: {}".format(type(attr)))
# first check that the attribute does not exist on the class
if hasattr(self, attr):
raise ValueError("will not overwrite attribute '{}'".format(attr))
try:
assert attr not in self._attrs
except AssertionError:
raise ValueError("attribute '{}' already exists".format(attr))
if isinstance(value, Block):
self._attrs[attr] = value
self._is_parent = True
else:
self._attrs[attr] = value
def __setattr__(self, key, value):
"""Guard against unintentional modification of _attrs"""
if key == '_attrs': # we can't prevent it but we can control it's type and content
# value must be a dictionary
try:
assert isinstance(value, dict)
except AssertionError:
raise ValueError('{} must be a dict'.format(key))
# value must have strings for keys
try:
keys = list(value.keys())
keys_are_strings = map(lambda x: isinstance(x, str), keys)
assert all(keys_are_strings) # or len(keys) == 0
except AssertionError:
raise ValueError("all keys of {} must be strings".format(key))
super(Block, self).__setattr__(key, value)
# todo: rename to 'rename_attr'
# todo: change signature to rename(self, name, new_name)
def move_attr(self, new_name, name):
"""Rename an attribute"""
try:
assert new_name not in self._attrs
except AssertionError:
raise ValueError("will not overwrite attribute '{}'".format(new_name))
else:
try:
self._attrs[new_name] = self._attrs[name]
del self._attrs[name]
except KeyError:
raise AttributeError('''no attribute '{}' found'''.format(name))
def rename_attr(self, attr, new_name):
self.move_attr(new_name, attr)
def __getattr__(self, name):
try:
return self._attrs[name]
except KeyError:
raise AttributeError('''attribute {} not found'''.format(name))
def __str__(self, prefix="", index=None,alt_name=None):
"""Compile the hierarchy of Blocks into a tree
:param str prefix: prefix to signify depth in the tree
:param int index: applies for list items [default: None]
:returns str string: formatted string of attributes
"""
# the root Block will have an empty prefix
# but the prefix will be updated calls for __str__ for nested Blocks
# we use the format() function to pass a format_spec which does alignment
string = ''
if index is not None:
string += "{} {} [is_parent? {:<5}]\n".format(
format(prefix + "+[{}]-{}".format(index, self.name if alt_name in (self.name,None,'') else alt_name), '<55'),
format(type(self).__name__, '>50'),
str(self.is_parent)
)
else:
name = format(prefix + "+-{}".format(self.name if alt_name in (self.name,None,'') else alt_name), '<55')
if len(name) > 55:
name = name[:52] + '...'
string += "{} {} [is_parent? {:<5}]\n".format(
name,
format(type(self).__name__, '>50'),
str(self.is_parent)
)
for attr in self._attrs:
# check if the attribute is Block or non-Block
if isinstance(self._attrs[attr], Block):
# if it is a Block then the prefix will change by having extra '| ' before it
# string += 'something\n'
string += self._attrs[attr].__str__(prefix=prefix + "| ",alt_name=attr)
else:
# if it is not a Block then we construct the repr. manually
val = self._attrs[attr]
# don't print the whole array for large arrays
if isinstance(val, (np.ndarray,)):
# we construct a tuple for the first array element (0,...,0) and the last
# one (-1,...,-1); however, we have to do this independent of the dimensions
# we use a tuple constructed using shape - 1 in both cases
start = tuple([0] * (len(val.shape) - 1))
end = tuple([-1] * (len(val.shape) - 1))
if start == end:
string += prefix + "| +-{}: {}\n".format(attr, val[start])
else:
string += prefix + "| +-{}: {},...,{}\n".format(attr, val[start], val[end])
else:
if isinstance(self._attrs[attr], str):
if attr == "@alias" and alt_name == self._attrs[attr]:
string += prefix + "| +-{}: {}\n".format(attr, self.name)
elif len(self._attrs[attr]) > 55:
string += prefix + "| +-{}: {}\n".format(attr, self._attrs[attr][:52] + '...')
else:
string += prefix + "| +-{}: {}\n".format(attr, self._attrs[attr])
else:
string += prefix + "| +-{}: {}\n".format(attr, self._attrs[attr])
return string
def __getitem__(self, index):
try:
assert isinstance(index, int)
except AssertionError:
raise ValueError('index must be an integer or long')
if self.name == 'Materials':
for attr in self.attrs():
block = getattr(self, attr)
if hasattr(block, 'Id'):
if getattr(block, 'Id') == index:
return block
else:
continue
return
def __contains__(self, item):
if item in self._attrs:
return True
return False
def __eq__(self, other):
try:
assert isinstance(other, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
if self.name == other.name:
return True
return False
def __le__(self, other):
try:
assert isinstance(other, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
if self.name < other.name:
return True
return False
class ListBlock(Block):
"""Data content block for sequence entities"""
__slots__ = ('_list', '_material_dict')
def __init__(self, *args, **kwargs):
super(ListBlock, self).__init__(*args, **kwargs)
self._list = list() # separate attribute for ease of management
self._material_dict = dict() # a dictionary used by Materials to extract material by material name
def items(self):
return self._list
@property
def ids(self):
ids = list()
if self.name == 'Materials':
# first check non-list items
for attr in self.attrs():
_attrval = getattr(self, attr)
if hasattr(_attrval, 'Id'):
ids.append(int(_attrval.Id))
elif hasattr(_attrval, 'id'):
ids.append(int(_attrval.id))
# then check list items
for list_item in self:
if hasattr(list_item, 'Id'):
ids.append(int(list_item.Id))
elif hasattr(list_item, 'id'):
ids.append(int(list_item.id))
return ids
@property
def material_dict(self):
"""A convenience dictionary of materials indexed by material name
If this is not a Materials ListBlock (name = 'Material') then it should return None
"""
# todo: testcase: name == 'Materials' ? dictionary of material blocks : None
return self._material_dict
@material_dict.setter
def material_dict(self, value):
"""Check that this is a material block"""
if self.name == "Materials":
if isinstance(value, dict):
keys_are_strings = map(lambda k: isinstance(k, str), value.keys())
values_are_blocks = map(lambda v: isinstance(v, Block), value.values())
try:
assert all(keys_are_strings)
except AssertionError:
raise ValueError("keys for material_dict dictionary must be strings")
try:
assert all(values_are_blocks)
except AssertionError:
raise ValueError("values for material_dict dictionary must be Blocks (or subclasses)")
# now we can set
self._material_dict = value
else:
raise TypeError("value must be a dict")
else:
raise ValueError("the material_dict attribute can only be set for Materials ListBlocks")
def __setattr__(self, key, value):
"""We do some sanity checks before allowing direct setting"""
# we only allow modification of _list if it meets some criteria
if key == '_list':
# it must be a list
try:
assert isinstance(value, list)
except AssertionError:
raise ValueError("_list attribute must be a list")
# make sure it's either empty or has block subclasses
if len(value) > 0:
try:
assert all(map(lambda x: isinstance(x, Block), value))
except AssertionError:
raise ValueError("list contains non-Block class/subclass")
super(ListBlock, self).__setattr__(key, value)
@property
def is_parent(self):
"""A ListBlock is a parent if it has a Block attribute or if it has list items"""
# todo: testcase
if super(ListBlock, self)._is_parent:
return True
else:
if len(self._list) > 0:
return True
else:
return False
def __str__(self, prefix="", index=None,alt_name=None):
"""Convert the ListBlock into a string
:param str prefix: prefix to signify depth in the tree
:param int index: applies for list items [default: None]
:returns str string: formatted string of attributes
"""
# first we use the superclass to populate everything else
string = super(ListBlock, self).__str__(prefix=prefix, index=index,alt_name=alt_name)
# now we stringify the list-blocks
for index, block in enumerate(self.items()):
string += block.__str__(prefix=prefix + "| ", index=index)
return string
def __len__(self):
return len(self._list)
def __setitem__(self, key, value):
try:
assert isinstance(value, Block)
except AssertionError:
raise ValueError('value must be a Block class/subclass')
try:
self._list[key] = value
except IndexError:
self._list.append(value)
# raise ValueError("index {} does not exist".format(key))
def __getitem__(self, item):
try:
return self._list[item]
except KeyError:
raise IndexError("no item with index '{}'".format(item))
def __iter__(self):
return iter(self._list)
def __contains__(self, item):
if item in self._list:
return True
return False
def __delitem__(self, key):
try:
del self._list[key]
except KeyError:
raise IndexError("missing item at index'{}'".format(key))
# Mutable sequences should provide methods append(), count(), index(), extend(), insert(), pop(), remove(),
# reverse() and sort(), like Python standard list objects.
def append(self, item):
try:
assert isinstance(item, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
self._list.append(item)
def count(self, item, *args):
try:
assert isinstance(item, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
return self._list.count(item, *args)
def index(self, item, *args):
try:
assert isinstance(item, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
return self._list.index(item, *args)
def extend(self, item):
try:
assert isinstance(item, list)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
self._list.extend(item)
def insert(self, index, item):
try:
assert isinstance(item, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
self._list.insert(index, item)
def pop(self, *args):
return self._list.pop(*args)
def remove(self, item):
try:
assert isinstance(item, Block)
except AssertionError:
raise ValueError('item must be a Block class/subclass')
return self._list.remove(item)
def reverse(self):
self._list.reverse()
def sort(self, **kwargs):
return self._list.sort(**kwargs)