Source code for nicetoolbox.utils.check_and_exception
"""
Check and exception handling functions.
"""
import logging
import os
import numpy as np
from . import filehandling as fh
[docs]def check_token_in_filepath(folder_name: str, token: str, description: str) -> None:
"""
Check if a given token is present in a folder name.
Args:
folder_name (str): The name of the folder to check.
token (str): The token to search for in the folder name.
description (str): A description of the folder for error messages.
Raises:
ValueError: If the token is not found in the folder name.
Returns:
None
"""
if token not in folder_name:
raise ValueError(
f"The given {description} '{folder_name}' does not contain "
f"the required token '{token}'."
)
[docs]def check_options(object, object_type, options) -> None:
"""
Check if an object is of a specific type and if it is within a given
list of options.
Args:
object (any): The object to be checked.
object_type (type): The expected type of the object.
options (list): A list of valid options for the object.
Raises:
TypeError: If the object is not of the expected type.
ValueError: If the object is not within the list of valid options.
Returns:
None
"""
if not isinstance(object, object_type):
raise TypeError(
f"Expected object of type {object_type.__name__}, "
f"got {type(object).__name__}"
)
if object not in options:
raise ValueError(
f"Object {object} is not in the list of valid options: {options}"
)
[docs]def check_value_bounds(
object, object_type=None, object_min=None, object_max=None
) -> None:
"""
Check if an object's value falls within specified bounds.
Args:
object (any): The object to be checked.
object_type (type, optional): The expected type of the object. Defaults to None.
object_min (any, optional): The minimum value allowed for the object.
Defaults to None.
object_max (any, optional): The maximum value allowed for the object.
Defaults to None.
Raises:
TypeError: If the object is not of the expected type (if object_type is
provided).
ValueError: If the object's value is less than object_min (if object_min
is provided).
If the object's value is greater than object_max (if object_max
is provided).
Returns:
None
"""
if object_type is not None and not isinstance(object, object_type):
raise TypeError(
f"Expected object of type {object_type.__name__}, "
f"got {type(object).__name__}"
)
if object_min is not None and object < object_min:
raise ValueError(
f"Object value {object} is less than the minimum allowed value {object_min}"
)
if object_max is not None and object > object_max:
raise ValueError(
f"Object value {object} is greater than the maximum allowed "
f"value {object_max}"
)
[docs]def file_exists(file: str) -> None:
"""
Check if a file exists at the given path.
Args:
file (str): The path to the file to check.
Raises:
FileNotFoundError: If the file does not exist at the given path.
Returns:
None
"""
if not os.path.exists(file):
raise FileNotFoundError
[docs]def error_log_and_raise(error, name, message):
"""
This function logs an error message and then raises a specific error with
a formatted message.
Args:
error (Exception): The type of error to be raised.
name (str): The name of the function or method where the error occurred.
message (str): The detailed error message.
Raises:
error: The specific error type raised with a formatted error message.
"""
logging.error(f"{name}: {error.__name__}. {message}")
raise error(f"{name}: {message}")
[docs]def check_user_input_config(config, check, config_name, var=None):
"""
Check a given configuration dictionary for correct user inputs
based on a template dictionary describing the valid inputs.
Args:
config (dict): The config that contains all inputs to be checked.
check (dict): The template specifying which inputs are valid for
each dict key.
config_name (str): The name of the config to be checked,
used for more descriptive logs.
Note:
The keys of 'check' must include the keys of 'config'.
Syntax of the dict values:
'type:<str/int/bool/...>': specifies the valid data type
'folder:<base/full>': requires existence of the folder (in case of 'full')
or parent-folder (in case of 'base')
'file': requires that the file is existing on the system
'keys:<toml_filepath>': valid options are all keys from the dict given
by the toml_filepath
'tbd': not yet defined in the template dict 'check',
will write a warning to log
[<>, ...]: list of valid options, may contain all basetypes
"""
# the config dict contains all test-keys and test-values (tkey, tval)
# the check dict contains all check-keys and check-values (ckey, cval)
for tkey, tval in config.items():
# CHECKING KEYS
# given a definition of valid keys
if "_valid_keys_" in check:
check_user_input_config(
{"test": tkey}, {"test": check["_valid_keys_"]}, config_name, var
)
# if a key serves as a variable
if list(check.keys()) == ["_var_"] or set(["_var_", "_valid_keys_"]) == set(
list(check.keys())
):
var = tkey
cval = check["_var_"]
# if not, the validity criterion for its value(s) needs to be defined in the
# check dictionary
elif tkey not in check:
error_log_and_raise(
LookupError,
config_name,
f"'{tkey}' not found in within the keys of the check dictionary.",
)
break
# if key is no variable and validity criterion for its value is given, use
# this for checks
else:
cval = check[tkey]
# CHECKING VALUES
# special case: the test value is a list
if isinstance(tval, list):
if tval == []:
pass
else:
for tvalue in tval:
check_user_input_config({tkey: tvalue}, check, config_name, var)
# go through all options for the check-value (cval)
elif isinstance(cval, str):
# check whether there is a variable in the string to replace
if "_var_" in cval:
if var is not None:
cval = cval.replace("_var_", var)
else:
error_log_and_raise(
LookupError,
config_name,
f"value '{cval}' requires the variable (_var_) "
"to be defined.",
)
# type check
if cval.startswith("type"):
check_type = cval.split(":")[1]
if not isinstance(tval, __builtins__[check_type]):
error_log_and_raise(
TypeError,
config_name,
f"Key '{tkey}' requires value of type {check_type}. "
f"The given value is '{tval}'.",
)
elif cval.startswith("folder"):
folder_details = cval.split(":")[1]
if folder_details == "base":
folder = os.path.dirname(tval)
elif folder_details == "full":
folder = tval
else:
error_log_and_raise(
ValueError,
config_name,
f"Unknown check value {folder_details}. "
"Options are 'base' or 'full'.",
)
if not os.path.isdir(folder):
error_log_and_raise(
NotADirectoryError,
config_name,
f"For key '{tkey}', the folder '{folder}' "
"is not a directory.",
)
elif cval.startswith("file"):
if not os.path.isfile(tval):
error_log_and_raise(
FileNotFoundError,
config_name,
f"For key {tkey}, the file '{tval}' is not found.",
)
elif cval.startswith("keys"):
keys = load_dict_keys_values(cval, config_name)
check_user_input_config({tkey: tval}, {tkey: keys}, config_name, var)
elif cval.startswith("values"):
values = load_dict_keys_values(cval, config_name)
check_user_input_config({tkey: tval}, {tkey: values}, config_name, var)
elif cval.startswith("tbd"):
continue
else:
error_log_and_raise(
NotImplementedError,
config_name,
f"Check option '{cval.split(':')[0]}' is unknown. Currently "
"supported options in strings are 'type', 'folder', "
"'file', 'keys', 'tbd'.",
)
# check given options
elif isinstance(cval, list):
if tval not in cval:
error_log_and_raise(
ValueError,
config_name,
f"Key '{tkey}' can take values {cval}. "
f"The given value is '{tval}'.",
)
# recursive strategy for dicts
elif isinstance(cval, dict):
check_user_input_config(tval, cval, config_name, var)
[docs]def load_dict_keys_values(command: str, config_name: str) -> list:
"""
Load keys or values from a .toml file based on the given command.
Args:
command (str): A string in the format 'token:file:key(s)' or 'token:file'.
'token' should be either 'keys' or 'values'.
'file' is the path to the .toml file.
'key(s)' is an optional parameter specifying the nested keys in the
.toml file.
config_name (str): The name of the configuration for error logging.
Returns:
list: A list of keys or values from the .toml file based on the given command.
Raises:
NotImplementedError: If the file format is not supported.
AssertionError: If the token is neither 'keys' nor 'values'.
"""
has_sub_keys = len(command.split(":")) != 2
if not has_sub_keys:
token, file = command.split(":")
else:
token, file, sub_keys = command.split(":")
# ensure that token == 'keys' or 'values'
check_options(token, str, ["keys", "values"])
if file.endswith(".toml"):
loaded_dict = fh.load_config(file)
if has_sub_keys:
for sub_key in sub_keys.split("."):
loaded_dict = loaded_dict[sub_key]
# get keys or values of the loaded dict
keys_values = loaded_dict.keys() if token == "keys" else loaded_dict
else:
error_log_and_raise(
NotImplementedError,
config_name,
f"Check option '{token}:<>' currently only supports '.toml' files "
f"to retrieve keys. Given argument: '{file}'",
)
return list(keys_values)
[docs]def check_zeros(arr: np.ndarray) -> None:
"""
Check if any vectors in the last dimension of an array are zero vectors.
Args
arr (ndarray): The input array.
Raises
AssertionError: If there are any zero vectors found in the last
dimension of the array.
Examples
>>> arr_3d_example = np.random.randint(0, 255, (10, 10, 3))
>>> check_zeros(arr_3d_example)
"""
# Determine the size of the last dimension from the input array
last_dim_size = arr.shape[-1]
zero_vector = np.zeros(last_dim_size)
zero_cells = np.all(arr == zero_vector, axis=-1)
if np.any(zero_cells):
logging.warning(
f" Warning. Data array contains zero cells at {np.argwhere(zero_cells)}"
)