wc_utils/util/types.py
""" Utility functions
:Author: Jonathan Karr <karr@mssm.edu>
:Date: 2016-08-20
:Copyright: 2016-2018, Karr Lab
:License: MIT
"""
import numpy as np
from wc_utils.util.list import det_dedupe
def cast_to_builtins(obj):
""" Recursively type cast an object to a semantically equivalent object expressed using only builtin types
* All iterable objects (objects with `__iter__` attribute) are converted to lists
* All dictionable objects (objects which are dictionaries or which have the `__dict__` attribute) are
converted to dictionaries
Args:
obj (:obj:`object`): an object
Returns:
:obj:`object`: a semantically equivalent object expressed using only builtin types
"""
if isinstance(obj, dict):
return dict((key, cast_to_builtins(val)) for key, val in obj.items())
elif hasattr(obj, '__dict__'):
return dict((key, cast_to_builtins(val)) for key, val in obj.__dict__.items())
elif hasattr(obj, '__iter__') and not isinstance(obj, str):
return [cast_to_builtins(val) for val in obj]
if isinstance(obj, (np.bool_, np.int_, np.intc, np.intp,
np.int8, np.int16, np.int32, np.int64,
np.uint8, np.uint16, np.uint32, np.uint64,
np.float16, np.float32, np.float64,
np.complex64, np.complex128,
)):
return obj.item()
else:
return obj
def assert_value_equal(obj1, obj2, check_type=False, check_iterable_ordering=False):
""" Recursively raise an exception if two objects have different semantic values, ignoring
* key/attribute order
* optionally, object types
* optionally, element ordering in iterables
Args:
obj1 (:obj:`object`): first object
obj1 (:obj:`object`): second object
check_type (:obj:`bool`, optional): If true, raise an exception if `obj1` and `obj2` have different types
check_iterable_ordering (:obj:`bool`, optional): If true, raise an exception if the objects have different
orderings of iterable attributes
Raises:
obj:`TypesUtilAssertionError`: If the value of `obj1` is not equal to that of `obj2`
"""
if check_type and obj1.__class__ != obj2.__class__:
raise TypesUtilAssertionError('Type of obj1 ({}) is not equal to that of obj2 ({})'.format(
obj1.__class__, obj2.__class__))
if isinstance(obj1, dict) or hasattr(obj1, '__dict__'):
if not (isinstance(obj2, dict) or hasattr(obj2, '__dict__')):
raise TypesUtilAssertionError(
'obj1 has attributes/keys, but obj2 does not.\n\nobj1:\n{}\n\nobj2:\n{}'.format(obj1, obj2))
if isinstance(obj1, dict):
attr1 = obj1.keys()
else:
attr1 = vars(obj1)
if isinstance(obj2, dict):
attr2 = obj2.keys()
else:
attr2 = vars(obj2)
if set(attr1) != set(attr2):
raise TypesUtilAssertionError('Objects have different attributes/keys')
for attr in attr1:
if isinstance(obj1, dict):
val1 = obj1[attr]
else:
val1 = getattr(obj1, attr)
if isinstance(obj2, dict):
val2 = obj2[attr]
else:
val2 = getattr(obj2, attr)
assert_value_equal(val1, val2, check_type, check_iterable_ordering)
elif (hasattr(obj1, '__iter__') and not (isinstance(obj1, dict) or hasattr(obj1, '__dict__') or
isinstance(obj1, str))):
if not ((hasattr(obj2, '__iter__') and not (isinstance(obj2, dict) or
hasattr(obj2, '__dict__') or isinstance(obj2, str)))):
raise TypesUtilAssertionError('obj1 is iterable, but obj2 is not')
if len(obj1) != len(obj2):
raise TypesUtilAssertionError('Objects have different lengths')
if check_iterable_ordering:
for val1, val2 in zip(obj1, obj2):
assert_value_equal(val1, val2, check_type, check_iterable_ordering)
else:
used2 = []
for val1 in obj1:
matching_val2 = False
for i2, val2 in enumerate(obj2):
if i2 in used2:
continue
try:
assert_value_equal(val1, val2, check_type, check_iterable_ordering)
matching_val2 = True
used2.append(i2)
break
except TypesUtilAssertionError:
pass
if not matching_val2:
raise TypesUtilAssertionError('No equivalent element {} in obj2'.format(val1))
else:
try:
if np.isnan(obj1) and np.isnan(obj2):
return
except:
pass
if obj1 != obj2:
raise TypesUtilAssertionError('Objects have different values')
def assert_value_not_equal(obj1, obj2, check_type=False, check_iterable_ordering=False):
""" Recursively raise an exception if two objects have the same semantic values, ignoring
* key/attribute order
* optionally, object types
* optionally, element ordering in iterables
Args:
obj1 (:obj:`object`): first object
obj1 (:obj:`object`): second object
check_type (:obj:`bool`, optional): If true, raise an exception if `obj1` and `obj2` have different types
check_iterable_ordering (:obj:`bool`, optional): If true, raise an exception if the objects have different
orderings of iterable attributes
Raises:
obj:`TypesUtilAssertionError`: If the value of `obj1` is not equal to that of `obj2`
"""
try:
assert_value_equal(obj1, obj2, check_type=check_type,
check_iterable_ordering=check_iterable_ordering)
except TypesUtilAssertionError:
return
raise TypesUtilAssertionError('obj1 and obj2 have equal values')
def is_iterable(obj):
""" Check if object is an iterable (list, tuple, etc.) and not a string
Args:
obj (:obj:`object`): object
Returns:
:obj:`bool`: Whether or not object is iterable
"""
return hasattr(obj, '__iter__') \
and not isinstance(obj, (str, dict)) \
and not hasattr(obj, '__dict__')
def get_subclasses(cls, immediate_only=False):
""" Reproducibly get subclasses of a class, with duplicates removed
Args:
cls (:obj:`type`): class
immediate_only (:obj:`bool`, optional): if true, only return direct subclasses
Returns:
:obj:`list` of `type`: list of subclasses, with duplicates removed
"""
subclasses = list(cls.__subclasses__())
if not immediate_only:
for sub_cls in subclasses.copy():
subclasses.extend(get_subclasses(sub_cls, immediate_only=False))
return det_dedupe(subclasses)
def get_superclasses(cls, immediate_only=False):
""" Get superclasses of a class. If `immediate_only`, only return direct superclasses.
Args:
cls (:obj:`type`): class
immediate_only (:obj:`bool`): if true, only return direct superclasses
Returns:
:obj:`list` of :obj:`type`: list of superclasses
"""
superclasses = list(cls.__bases__)
if not immediate_only:
for superclass in cls.__bases__:
superclasses += get_superclasses(superclass)
return tuple(superclasses)
class TypesUtilAssertionError(AssertionError):
""" Types Util assertion error """
pass