ai-content-maker/.venv/Lib/site-packages/coqpit/coqpit.py

955 lines
33 KiB
Python
Raw Normal View History

2024-05-03 04:18:51 +03:00
import argparse
import functools
import json
import operator
import os
from collections.abc import MutableMapping
from dataclasses import MISSING as _MISSING
from dataclasses import Field, asdict, dataclass, fields, is_dataclass, replace
from pathlib import Path
from pprint import pprint
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union, get_type_hints
T = TypeVar("T")
MISSING: Any = "???"
class _NoDefault(Generic[T]):
pass
NoDefaultVar = Union[_NoDefault[T], T]
no_default: NoDefaultVar = _NoDefault()
def is_primitive_type(arg_type: Any) -> bool:
"""Check if the input type is one of `int, float, str, bool`.
Args:
arg_type (typing.Any): input type to check.
Returns:
bool: True if input type is one of `int, float, str, bool`.
"""
try:
return isinstance(arg_type(), (int, float, str, bool))
except (AttributeError, TypeError):
return False
def is_list(arg_type: Any) -> bool:
"""Check if the input type is `list`
Args:
arg_type (typing.Any): input type.
Returns:
bool: True if input type is `list`
"""
try:
return arg_type is list or arg_type is List or arg_type.__origin__ is list or arg_type.__origin__ is List
except AttributeError:
return False
def is_dict(arg_type: Any) -> bool:
"""Check if the input type is `dict`
Args:
arg_type (typing.Any): input type.
Returns:
bool: True if input type is `dict`
"""
try:
return arg_type is dict or arg_type is Dict or arg_type.__origin__ is dict
except AttributeError:
return False
def is_union(arg_type: Any) -> bool:
"""Check if the input type is `Union`.
Args:
arg_type (typing.Any): input type.
Returns:
bool: True if input type is `Union`
"""
try:
return safe_issubclass(arg_type.__origin__, Union)
except AttributeError:
return False
def safe_issubclass(cls, classinfo) -> bool:
"""Check if the input type is a subclass of the given class.
Args:
cls (type): input type.
classinfo (type): parent class.
Returns:
bool: True if the input type is a subclass of the given class
"""
try:
r = issubclass(cls, classinfo)
except Exception: # pylint: disable=broad-except
return cls is classinfo
else:
return r
def _coqpit_json_default(obj: Any) -> Any:
if isinstance(obj, Path):
return str(obj)
raise TypeError(f"Can't encode object of type {type(obj).__name__}")
def _default_value(x: Field):
"""Return the default value of the input Field.
Args:
x (Field): input Field.
Returns:
object: default value of the input Field.
"""
if x.default not in (MISSING, _MISSING):
return x.default
if x.default_factory not in (MISSING, _MISSING):
return x.default_factory()
return x.default
def _is_optional_field(field) -> bool:
"""Check if the input field is optional.
Args:
field (Field): input Field to check.
Returns:
bool: True if the input field is optional.
"""
# return isinstance(field.type, _GenericAlias) and type(None) in getattr(field.type, "__args__")
return type(None) in getattr(field.type, "__args__")
def my_get_type_hints(
cls,
):
"""Custom `get_type_hints` dealing with https://github.com/python/typing/issues/737
Returns:
[dataclass]: dataclass to get the type hints of its fields.
"""
r_dict = {}
for base in cls.__class__.__bases__:
if base == object:
break
r_dict.update(my_get_type_hints(base))
r_dict.update(get_type_hints(cls))
return r_dict
def _serialize(x):
"""Pick the right serialization for the datatype of the given input.
Args:
x (object): input object.
Returns:
object: serialized object.
"""
if isinstance(x, Path):
return str(x)
if isinstance(x, dict):
return {k: _serialize(v) for k, v in x.items()}
if isinstance(x, list):
return [_serialize(xi) for xi in x]
if isinstance(x, Serializable) or issubclass(type(x), Serializable):
return x.serialize()
if isinstance(x, type) and issubclass(x, Serializable):
return x.serialize(x)
return x
def _deserialize_dict(x: Dict) -> Dict:
"""Deserialize dict.
Args:
x (Dict): value to deserialized.
Returns:
Dict: deserialized dictionary.
"""
out_dict = {}
for k, v in x.items():
if v is None: # if {'key':None}
out_dict[k] = None
else:
out_dict[k] = _deserialize(v, type(v))
return out_dict
def _deserialize_list(x: List, field_type: Type) -> List:
"""Deserialize values for List typed fields.
Args:
x (List): value to be deserialized
field_type (Type): field type.
Raises:
ValueError: Coqpit does not support multi type-hinted lists.
Returns:
[List]: deserialized list.
"""
field_args = None
if hasattr(field_type, "__args__") and field_type.__args__:
field_args = field_type.__args__
elif hasattr(field_type, "__parameters__") and field_type.__parameters__:
# bandaid for python 3.6
field_args = field_type.__parameters__
if field_args:
if len(field_args) > 1:
raise ValueError(" [!] Coqpit does not support multi-type hinted 'List'")
field_arg = field_args[0]
# if field type is TypeVar set the current type by the value's type.
if isinstance(field_arg, TypeVar):
field_arg = type(x)
return [_deserialize(xi, field_arg) for xi in x]
return x
def _deserialize_union(x: Any, field_type: Type) -> Any:
"""Deserialize values for Union typed fields
Args:
x (Any): value to be deserialized.
field_type (Type): field type.
Returns:
[Any]: desrialized value.
"""
for arg in field_type.__args__:
# stop after first matching type in Union
try:
x = _deserialize(x, arg)
break
except ValueError:
pass
return x
def _deserialize_primitive_types(x: Union[int, float, str, bool], field_type: Type) -> Union[int, float, str, bool]:
"""Deserialize python primitive types (float, int, str, bool).
It handles `inf` values exclusively and keeps them float against int fields since int does not support inf values.
Args:
x (Union[int, float, str, bool]): value to be deserialized.
field_type (Type): field type.
Returns:
Union[int, float, str, bool]: deserialized value.
"""
if isinstance(x, (str, bool)):
return x
if isinstance(x, (int, float)):
if x == float("inf") or x == float("-inf"):
# if value type is inf return regardless.
return x
x = field_type(x)
return x
# TODO: Raise an error when x does not match the types.
return None
def _deserialize(x: Any, field_type: Any) -> Any:
"""Pick the right desrialization for the given object and the corresponding field type.
Args:
x (object): object to be deserialized.
field_type (type): expected type after deserialization.
Returns:
object: deserialized object
"""
# pylint: disable=too-many-return-statements
if is_dict(field_type):
return _deserialize_dict(x)
if is_list(field_type):
return _deserialize_list(x, field_type)
if is_union(field_type):
return _deserialize_union(x, field_type)
if issubclass(field_type, Serializable):
return field_type.deserialize_immutable(x)
if is_primitive_type(field_type):
return _deserialize_primitive_types(x, field_type)
raise ValueError(f" [!] '{type(x)}' value type of '{x}' does not match '{field_type}' field type.")
# Recursive setattr (supports dotted attr names)
def rsetattr(obj, attr, val):
def _setitem(obj, attr, val):
return operator.setitem(obj, int(attr), val)
pre, _, post = attr.rpartition(".")
setfunc = _setitem if post.isnumeric() else setattr
return setfunc(rgetattr(obj, pre) if pre else obj, post, val)
# Recursive getattr (supports dotted attr names)
def rgetattr(obj, attr, *args):
def _getitem(obj, attr):
return operator.getitem(obj, int(attr), *args)
def _getattr(obj, attr):
getfunc = _getitem if attr.isnumeric() else getattr
return getfunc(obj, attr, *args)
return functools.reduce(_getattr, [obj] + attr.split("."))
# Recursive setitem (supports dotted attr names)
def rsetitem(obj, attr, val):
pre, _, post = attr.rpartition(".")
return operator.setitem(rgetitem(obj, pre) if pre else obj, post, val)
# Recursive getitem (supports dotted attr names)
def rgetitem(obj, attr, *args):
def _getitem(obj, attr):
return operator.getitem(obj, int(attr) if attr.isnumeric() else attr, *args)
return functools.reduce(_getitem, [obj] + attr.split("."))
@dataclass
class Serializable:
"""Gives serialization ability to any inheriting dataclass."""
def __post_init__(self):
self._validate_contracts()
for key, value in self.__dict__.items():
if value is no_default:
raise TypeError(f"__init__ missing 1 required argument: '{key}'")
def _validate_contracts(self):
dataclass_fields = fields(self)
for field in dataclass_fields:
value = getattr(self, field.name)
if value is None:
if not _is_optional_field(field):
raise TypeError(f"{field.name} is not optional")
contract = field.metadata.get("contract", None)
if contract is not None:
if value is not None and not contract(value):
raise ValueError(f"break the contract for {field.name}, {self.__class__.__name__}")
def validate(self):
"""validate if object can serialize / deserialize correctly."""
self._validate_contracts()
if self != self.__class__.deserialize( # pylint: disable=no-value-for-parameter
json.loads(json.dumps(self.serialize()))
):
raise ValueError("could not be deserialized with same value")
def to_dict(self) -> dict:
"""Transform serializable object to dict."""
cls_fields = fields(self)
o = {}
for cls_field in cls_fields:
o[cls_field.name] = getattr(self, cls_field.name)
return o
def serialize(self) -> dict:
"""Serialize object to be json serializable representation."""
if not is_dataclass(self):
raise TypeError("need to be decorated as dataclass")
dataclass_fields = fields(self)
o = {}
for field in dataclass_fields:
value = getattr(self, field.name)
value = _serialize(value)
o[field.name] = value
return o
def deserialize(self, data: dict) -> "Serializable":
"""Parse input dictionary and desrialize its fields to a dataclass.
Returns:
self: deserialized `self`.
"""
if not isinstance(data, dict):
raise ValueError()
data = data.copy()
init_kwargs = {}
for field in fields(self):
# if field.name == 'dataset_config':
if field.name not in data:
if field.name in vars(self):
init_kwargs[field.name] = vars(self)[field.name]
continue
raise ValueError(f' [!] Missing required field "{field.name}"')
value = data.get(field.name, _default_value(field))
if value is None:
init_kwargs[field.name] = value
continue
if value == MISSING:
raise ValueError(f"deserialized with unknown value for {field.name} in {self.__name__}")
value = _deserialize(value, field.type)
init_kwargs[field.name] = value
for k, v in init_kwargs.items():
setattr(self, k, v)
return self
@classmethod
def deserialize_immutable(cls, data: dict) -> "Serializable":
"""Parse input dictionary and desrialize its fields to a dataclass.
Returns:
Newly created deserialized object.
"""
if not isinstance(data, dict):
raise ValueError()
data = data.copy()
init_kwargs = {}
for field in fields(cls):
# if field.name == 'dataset_config':
if field.name not in data:
if field.name in vars(cls):
init_kwargs[field.name] = vars(cls)[field.name]
continue
# if not in cls and the default value is not Missing use it
default_value = _default_value(field)
if default_value not in (MISSING, _MISSING):
init_kwargs[field.name] = default_value
continue
raise ValueError(f' [!] Missing required field "{field.name}"')
value = data.get(field.name, _default_value(field))
if value is None:
init_kwargs[field.name] = value
continue
if value == MISSING:
raise ValueError(f"Deserialized with unknown value for {field.name} in {cls.__name__}")
value = _deserialize(value, field.type)
init_kwargs[field.name] = value
return cls(**init_kwargs)
# ---------------------------------------------------------------------------- #
# Argument Parsing from `argparse` #
# ---------------------------------------------------------------------------- #
def _get_help(field):
try:
field_help = field.metadata["help"]
except KeyError:
field_help = ""
return field_help
def _init_argparse(
parser,
field_name,
field_type,
field_default,
field_default_factory,
field_help,
arg_prefix="",
help_prefix="",
relaxed_parser=False,
):
has_default = False
default = None
if field_default:
has_default = True
default = field_default
elif field_default_factory not in (None, _MISSING):
has_default = True
default = field_default_factory()
if not has_default and not is_primitive_type(field_type) and not is_list(field_type):
# aggregate types (fields with a Coqpit subclass as type) are not supported without None
return parser
arg_prefix = field_name if arg_prefix == "" else f"{arg_prefix}.{field_name}"
help_prefix = field_help if help_prefix == "" else f"{help_prefix} - {field_help}"
if is_dict(field_type): # pylint: disable=no-else-raise
# NOTE: accept any string in json format as input to dict field.
parser.add_argument(
f"--{arg_prefix}",
dest=arg_prefix,
default=json.dumps(field_default) if field_default else None,
type=json.loads,
)
elif is_list(field_type):
# TODO: We need a more clear help msg for lists.
if hasattr(field_type, "__args__"): # if the list is hinted
if len(field_type.__args__) > 1 and not relaxed_parser:
raise ValueError(" [!] Coqpit does not support multi-type hinted 'List'")
list_field_type = field_type.__args__[0]
else:
raise ValueError(" [!] Coqpit does not support un-hinted 'List'")
# TODO: handle list of lists
if is_list(list_field_type) and relaxed_parser:
return parser
if not has_default or field_default_factory is list:
if not is_primitive_type(list_field_type) and not relaxed_parser:
raise NotImplementedError(" [!] Empty list with non primitive inner type is currently not supported.")
# If the list's default value is None, the user can specify the entire list by passing multiple parameters
parser.add_argument(
f"--{arg_prefix}",
nargs="*",
type=list_field_type,
help=f"Coqpit Field: {help_prefix}",
)
else:
# If a default value is defined, just enable editing the values from argparse
# TODO: allow inserting a new value/obj to the end of the list.
for idx, fv in enumerate(default):
parser = _init_argparse(
parser,
str(idx),
list_field_type,
fv,
field_default_factory,
field_help="",
help_prefix=f"{help_prefix} - ",
arg_prefix=f"{arg_prefix}",
relaxed_parser=relaxed_parser,
)
elif is_union(field_type):
# TODO: currently I don't know how to handle Union type on argparse
if not relaxed_parser:
raise NotImplementedError(
" [!] Parsing `Union` field from argparse is not yet implemented. Please create an issue."
)
elif issubclass(field_type, Serializable):
return default.init_argparse(
parser, arg_prefix=arg_prefix, help_prefix=help_prefix, relaxed_parser=relaxed_parser
)
elif isinstance(field_type(), bool):
def parse_bool(x):
if x not in ("true", "false"):
raise ValueError(f' [!] Value for boolean field must be either "true" or "false". Got "{x}".')
return x == "true"
parser.add_argument(
f"--{arg_prefix}",
type=parse_bool,
default=field_default,
help=f"Coqpit Field: {help_prefix}",
metavar="true/false",
)
elif is_primitive_type(field_type):
parser.add_argument(
f"--{arg_prefix}",
default=field_default,
type=field_type,
help=f"Coqpit Field: {help_prefix}",
)
else:
if not relaxed_parser:
raise NotImplementedError(f" [!] '{field_type}' is not supported by arg_parser. Please file a bug report.")
return parser
# ---------------------------------------------------------------------------- #
# Main Coqpit Class #
# ---------------------------------------------------------------------------- #
@dataclass
class Coqpit(Serializable, MutableMapping):
"""Coqpit base class to be inherited by any Coqpit dataclasses.
It overrides Python `dict` interface and provides `dict` compatible API.
It also enables serializing/deserializing a dataclass to/from a json file, plus some semi-dynamic type and value check.
Note that it does not support all datatypes and likely to fail in some cases.
"""
_initialized = False
def _is_initialized(self):
"""Check if Coqpit is initialized. Useful to prevent running some aux functions
at the initialization when no attribute has been defined."""
return "_initialized" in vars(self) and self._initialized
def __post_init__(self):
self._initialized = True
try:
self.check_values()
except AttributeError:
pass
## `dict` API functions
def __iter__(self):
return iter(asdict(self))
def __len__(self):
return len(fields(self))
def __setitem__(self, arg: str, value: Any):
setattr(self, arg, value)
def __getitem__(self, arg: str):
"""Access class attributes with ``[arg]``."""
return self.__dict__[arg]
def __delitem__(self, arg: str):
delattr(self, arg)
def _keytransform(self, key): # pylint: disable=no-self-use
return key
## end `dict` API functions
def __getattribute__(self, arg: str): # pylint: disable=no-self-use
"""Check if the mandatory field is defined when accessing it."""
value = super().__getattribute__(arg)
if isinstance(value, str) and value == "???":
raise AttributeError(f" [!] MISSING field {arg} must be defined.")
return value
def __contains__(self, arg: str):
return arg in self.to_dict()
def get(self, key: str, default: Any = None):
if self.has(key):
return asdict(self)[key]
return default
def items(self):
return asdict(self).items()
def merge(self, coqpits: Union["Coqpit", List["Coqpit"]]):
"""Merge a coqpit instance or a list of coqpit instances to self.
Note that it does not pass the fields and overrides attributes with
the last Coqpit instance in the given List.
TODO: find a way to merge instances with all the class internals.
Args:
coqpits (Union[Coqpit, List[Coqpit]]): coqpit instance or list of instances to be merged.
"""
def _merge(coqpit):
self.__dict__.update(coqpit.__dict__)
self.__annotations__.update(coqpit.__annotations__)
self.__dataclass_fields__.update(coqpit.__dataclass_fields__)
if isinstance(coqpits, list):
for coqpit in coqpits:
_merge(coqpit)
else:
_merge(coqpits)
def check_values(self):
pass
def has(self, arg: str) -> bool:
return arg in vars(self)
def copy(self):
return replace(self)
def update(self, new: dict, allow_new=False) -> None:
"""Update Coqpit fields by the input ```dict```.
Args:
new (dict): dictionary with new values.
allow_new (bool, optional): allow new fields to add. Defaults to False.
"""
for key, value in new.items():
if allow_new:
setattr(self, key, value)
else:
if hasattr(self, key):
setattr(self, key, value)
else:
raise KeyError(f" [!] No key - {key}")
def pprint(self) -> None:
"""Print Coqpit fields in a format."""
pprint(asdict(self))
def to_dict(self) -> dict:
# return asdict(self)
return self.serialize()
def from_dict(self, data: dict) -> None:
self = self.deserialize(data) # pylint: disable=self-cls-assignment
@classmethod
def new_from_dict(cls: Serializable, data: dict) -> "Coqpit":
return cls.deserialize_immutable(data)
def to_json(self) -> str:
"""Returns a JSON string representation."""
return json.dumps(asdict(self), indent=4, default=_coqpit_json_default)
def save_json(self, file_name: str) -> None:
"""Save Coqpit to a json file.
Args:
file_name (str): path to the output json file.
"""
with open(file_name, "w", encoding="utf8") as f:
json.dump(asdict(self), f, indent=4)
def load_json(self, file_name: str) -> None:
"""Load a json file and update matching config fields with type checking.
Non-matching parameters in the json file are ignored.
Args:
file_name (str): path to the json file.
Returns:
Coqpit: new Coqpit with updated config fields.
"""
with open(file_name, "r", encoding="utf8") as f:
input_str = f.read()
dump_dict = json.loads(input_str)
# TODO: this looks stupid 💆
self = self.deserialize(dump_dict) # pylint: disable=self-cls-assignment
self.check_values()
@classmethod
def init_from_argparse(
cls, args: Optional[Union[argparse.Namespace, List[str]]] = None, arg_prefix: str = "coqpit"
) -> "Coqpit":
"""Create a new Coqpit instance from argparse input.
Args:
args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```.
arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed.
"""
if not args:
# If args was not specified, parse from sys.argv
parser = cls.init_argparse(cls, arg_prefix=arg_prefix)
args = parser.parse_args() # pylint: disable=E1120, E1111
if isinstance(args, list):
# If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace
parser = cls.init_argparse(cls, arg_prefix=arg_prefix)
args = parser.parse_args(args) # pylint: disable=E1120, E1111
# Handle list and object attributes with defaults, which can be modified
# directly (eg. --coqpit.list.0.val_a 1), by constructing real objects
# from defaults and passing those to `cls.__init__`
args_with_lists_processed = {}
class_fields = fields(cls)
for field in class_fields:
has_default = False
default = None
field_default = field.default if field.default is not _MISSING else None
field_default_factory = field.default_factory if field.default_factory is not _MISSING else None
if field_default:
has_default = True
default = field_default
elif field_default_factory:
has_default = True
default = field_default_factory()
if has_default and (not is_primitive_type(field.type) or is_list(field.type)):
args_with_lists_processed[field.name] = default
args_dict = vars(args)
for k, v in args_dict.items():
# Remove argparse prefix (eg. "--coqpit." if present)
if k.startswith(f"{arg_prefix}."):
k = k[len(f"{arg_prefix}.") :]
rsetitem(args_with_lists_processed, k, v)
return cls(**args_with_lists_processed)
def parse_args(
self, args: Optional[Union[argparse.Namespace, List[str]]] = None, arg_prefix: str = "coqpit"
) -> None:
"""Update config values from argparse arguments with some meta-programming ✨.
Args:
args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```.
arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed.
"""
if not args:
# If args was not specified, parse from sys.argv
parser = self.init_argparse(arg_prefix=arg_prefix)
args = parser.parse_args()
if isinstance(args, list):
# If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace
parser = self.init_argparse(arg_prefix=arg_prefix)
args = parser.parse_args(args)
args_dict = vars(args)
for k, v in args_dict.items():
if k.startswith(f"{arg_prefix}."):
k = k[len(f"{arg_prefix}.") :]
try:
rgetattr(self, k)
except (TypeError, AttributeError) as e:
raise Exception(f" [!] '{k}' not exist to override from argparse.") from e
rsetattr(self, k, v)
self.check_values()
def parse_known_args(
self,
args: Optional[Union[argparse.Namespace, List[str]]] = None,
arg_prefix: str = "coqpit",
relaxed_parser=False,
) -> List[str]:
"""Update config values from argparse arguments. Ignore unknown arguments.
This is analog to argparse.ArgumentParser.parse_known_args (vs parse_args).
Args:
args (namespace or list of str, optional): parsed argparse.Namespace or list of command line parameters. If unspecified will use a newly created parser with ```init_argparse()```.
arg_prefix: prefix to add to CLI parameters. Gets forwarded to ```init_argparse``` when ```args``` is not passed.
relaxed_parser (bool, optional): If True, do not force all the fields to have compatible types with the argparser. Defaults to False.
Returns:
List of unknown parameters.
"""
if not args:
# If args was not specified, parse from sys.argv
parser = self.init_argparse(arg_prefix=arg_prefix, relaxed_parser=relaxed_parser)
args, unknown = parser.parse_known_args()
if isinstance(args, list):
# If a list was passed in (eg. the second result of `parse_known_args`, run that through argparse first to get a parsed Namespace
parser = self.init_argparse(arg_prefix=arg_prefix, relaxed_parser=relaxed_parser)
args, unknown = parser.parse_known_args(args)
self.parse_args(args)
return unknown
def init_argparse(
self,
parser: Optional[argparse.ArgumentParser] = None,
arg_prefix="coqpit",
help_prefix="",
relaxed_parser=False,
) -> argparse.ArgumentParser:
"""Pass Coqpit fields as argparse arguments. This allows to edit values through command-line.
Args:
parser (argparse.ArgumentParser, optional): argparse.ArgumentParser instance. If unspecified a new one will be created.
arg_prefix (str, optional): Prefix to be used for the argument name. Defaults to 'coqpit'.
help_prefix (str, optional): Prefix to be used for the argument description. Defaults to ''.
relaxed_parser (bool, optional): If True, do not force all the fields to have compatible types with the argparser. Defaults to False.
Returns:
argparse.ArgumentParser: parser instance with the new arguments.
"""
if not parser:
parser = argparse.ArgumentParser()
class_fields = fields(self)
for field in class_fields:
if field.name in vars(self):
# use the current value of the field
# prevent dropping the current value
field_default = vars(self)[field.name]
else:
# use the default value of the field
field_default = field.default if field.default is not _MISSING else None
field_type = field.type
field_default_factory = field.default_factory
field_help = _get_help(field)
_init_argparse(
parser,
field.name,
field_type,
field_default,
field_default_factory,
field_help,
arg_prefix,
help_prefix,
relaxed_parser,
)
return parser
def check_argument(
name,
c,
is_path: bool = False,
prerequest: str = None,
enum_list: list = None,
max_val: float = None,
min_val: float = None,
restricted: bool = False,
alternative: str = None,
allow_none: bool = True,
) -> None:
"""Simple type and value checking for Coqpit.
It is intended to be used under ```__post_init__()``` of config dataclasses.
Args:
name (str): name of the field to be checked.
c (dict): config dictionary.
is_path (bool, optional): if ```True``` check if the path is exist. Defaults to False.
prerequest (list or str, optional): a list of field name that are prerequestedby the target field name.
Defaults to ```[]```.
enum_list (list, optional): list of possible values for the target field. Defaults to None.
max_val (float, optional): maximum possible value for the target field. Defaults to None.
min_val (float, optional): minimum possible value for the target field. Defaults to None.
restricted (bool, optional): if ```True``` the target field has to be defined. Defaults to False.
alternative (str, optional): a field name superceding the target field. Defaults to None.
allow_none (bool, optional): if ```True``` allow the target field to be ```None```. Defaults to False.
Example:
>>> num_mels = 5
>>> check_argument('num_mels', c, restricted=True, min_val=10, max_val=2056)
>>> fft_size = 128
>>> check_argument('fft_size', c, restricted=True, min_val=128, max_val=4058)
"""
# check if None allowed
if allow_none and c[name] is None:
return
if not allow_none:
assert c[name] is not None, f" [!] None value is not allowed for {name}."
# check if restricted and it it is check if it exists
if isinstance(restricted, bool) and restricted:
assert name in c.keys(), f" [!] {name} not defined in config.json"
# check prerequest fields are defined
if isinstance(prerequest, list):
assert any(
f not in c.keys() for f in prerequest
), f" [!] prequested fields {prerequest} for {name} are not defined."
else:
assert (
prerequest is None or prerequest in c.keys()
), f" [!] prequested fields {prerequest} for {name} are not defined."
# check if the path exists
if is_path:
assert os.path.exists(c[name]), f' [!] path for {name} ("{c[name]}") does not exist.'
# skip the rest if the alternative field is defined.
if alternative in c.keys() and c[alternative] is not None:
return
# check value constraints
if name in c.keys():
if max_val is not None:
assert c[name] <= max_val, f" [!] {name} is larger than max value {max_val}"
if min_val is not None:
assert c[name] >= min_val, f" [!] {name} is smaller than min value {min_val}"
if enum_list is not None:
assert c[name].lower() in enum_list, f" [!] {name} is not a valid value"