from collections import Iterable from numbers import Integral, Real import numpy as np def _isinstance(value, expected_type): """A Numpy-aware replacement for isinstance This function will be obsolete when Numpy v. >= 1.9 is established. """ # Declare numpy numeric types. np_ints = (np.int_, np.intc, np.intp, np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64) np_floats = (np.float_, np.float16, np.float32, np.float64) # Include numpy integers, if necessary. if type(expected_type) is tuple: if Integral in expected_type: expected_type = expected_type + np_ints elif expected_type is Integral: expected_type = (Integral, ) + np_ints # Include numpy floats, if necessary. if type(expected_type) is tuple: if Real in expected_type: expected_type = expected_type + np_floats elif expected_type is Real: expected_type = (Real, ) + np_floats # Now, make the instance check. return isinstance(value, expected_type) def check_type(name, value, expected_type, expected_iter_type=None): """Ensure that an object is of an expected type. Optionally, if the object is iterable, check that each element is of a particular type. Parameters ---------- name : str Description of value being checked value : object Object to check type of expected_type : type type to check object against expected_iter_type : type or None, optional Expected type of each element in value, assuming it is iterable. If None, no check will be performed. """ if not _isinstance(value, expected_type): msg = 'Unable to set "{0}" to "{1}" which is not of type "{2}"'.format( name, value, expected_type.__name__) raise ValueError(msg) if expected_iter_type: for item in value: if not _isinstance(item, expected_iter_type): msg = 'Unable to set "{0}" to "{1}" since each item must be ' \ 'of type "{2}"'.format(name, value, expected_iter_type.__name__) raise ValueError(msg) def check_iterable_type(name, value, expected_type, min_depth=1, max_depth=1): """Ensure that an object is an iterable containing an expected type. Parameters ---------- name : str Description of value being checked value : Iterable Iterable, possibly of other iterables, that should ultimately contain the expected type expected_type : type type that the iterable should contain min_depth : int The minimum number of layers of nested iterables there should be before reaching the ultimately contained items max_depth : int The maximum number of layers of nested iterables there should be before reaching the ultimately contained items """ # Initialize the tree at the very first item. tree = [value] index = [0] # Traverse the tree. while index[0] != len(tree[0]): # If we are done with this level of the tree, go to the next branch on # the level above this one. if index[-1] == len(tree[-1]): del index[-1] del tree[-1] index[-1] += 1 continue # Get a string representation of the current index in case we raise an # exception. form = '[' + '{:d}, '*(len(index)-1) + '{:d}]' ind_str = form.format(*index) # What is the current item we are looking at? current_item = tree[-1][index[-1]] # If this item is of the expected type, then we've reached the bottom # level of this branch. if _isinstance(current_item, expected_type): # Is this deep enough? if len(tree) < min_depth: msg = 'Error setting "{0}": The item at {1} does not meet the '\ 'minimum depth of {2}'.format(name, ind_str, min_depth) raise ValueError(msg) # This item is okay. Move on to the next item. index[-1] += 1 # If this item is not of the expected type, then it's either an error or # another level of the tree that we need to pursue deeper. else: if isinstance(current_item, Iterable): # The tree goes deeper here, let's explore it. tree.append(current_item) index.append(0) # But first, have we exceeded the max depth? if len(tree) > max_depth: msg = 'Error setting {0}: Found an iterable at {1}, items '\ 'in that iterable excceed the maximum depth of {2}' \ .format(name, ind_str, max_depth) raise ValueError(msg) else: # This item is completely unexpected. msg = "Error setting {0}: Items must be of type '{1}', but " \ "item at {2} is of type '{3}'"\ .format(name, expected_type.__name__, ind_str, type(current_item).__name__) raise ValueError(msg) def check_length(name, value, length_min, length_max=None): """Ensure that a sized object has length within a given range. Parameters ---------- name : str Description of value being checked value : collections.Sized Object to check length of length_min : int Minimum length of object length_max : int or None, optional Maximum length of object. If None, it is assumed object must be of length length_min. """ if length_max is None: if len(value) != length_min: msg = 'Unable to set "{0}" to "{1}" since it must be of ' \ 'length "{2}"'.format(name, value, length_min) raise ValueError(msg) elif not length_min <= len(value) <= length_max: if length_min == length_max: msg = 'Unable to set "{0}" to "{1}" since it must be of ' \ 'length "{2}"'.format(name, value, length_min) else: msg = 'Unable to set "{0}" to "{1}" since it must have length ' \ 'between "{2}" and "{3}"'.format(name, value, length_min, length_max) raise ValueError(msg) def check_value(name, value, accepted_values): """Ensure that an object's value is contained in a set of acceptable values. Parameters ---------- name : str Description of value being checked value : collections.Iterable Object to check accepted_values : collections.Container Container of acceptable values """ if value not in accepted_values: msg = 'Unable to set "{0}" to "{1}" since it is not in "{2}"'.format( name, value, accepted_values) raise ValueError(msg) def check_less_than(name, value, maximum, equality=False): """Ensure that an object's value is less than a given value. Parameters ---------- name : str Description of the value being checked value : object Object to check maximum : object Maximum value to check against equality : bool, optional Whether equality is allowed. Defaluts to False. """ if equality: if value > maximum: msg = 'Unable to set "{0}" to "{1}" since it is greater than ' \ '"{2}"'.format(name, value, maximum) raise ValueError(msg) else: if value >= maximum: msg = 'Unable to set "{0}" to "{1}" since it is greater than ' \ 'or equal to "{2}"'.format(name, value, maximum) raise ValueError(msg) def check_greater_than(name, value, minimum, equality=False): """Ensure that an object's value is less than a given value. Parameters ---------- name : str Description of the value being checked value : object Object to check minimum : object Minimum value to check against equality : bool, optional Whether equality is allowed. Defaluts to False. """ if equality: if value < minimum: msg = 'Unable to set "{0}" to "{1}" since it is less than ' \ '"{2}"'.format(name, value, minimum) raise ValueError(msg) else: if value <= minimum: msg = 'Unable to set "{0}" to "{1}" since it is less than ' \ 'or equal to "{2}"'.format(name, value, minimum) raise ValueError(msg)