"""Validations for the arguments."""
from inspect import BoundArguments, signature
from typing import Any, Callable, List, Tuple, Type, Union, get_args, get_origin
__all__ = ['Validator']
[docs]
class Validator:
[docs]
@classmethod
def validate_argument_type(cls, value, typelist: List[Type]) -> None:
"""Validate the type of an argument.
Args:
value (Any): The value to be validated.
typelist (List[Type]): The list of types to be validated.
Raises:
TypeError: If the type of the argument is not in the typelist.
"""
if not isinstance(typelist, list):
typelist = [typelist]
if any([isinstance(value, tp) for tp in typelist]):
return
raise TypeError(
f'Invalid argument type, expected {[tp.__name__ for tp in typelist]} (got {value.__class__.__name__} instead).'
)
[docs]
@classmethod
def validate_vector(cls, value, length: int):
"""Validate the type and length of a vector.
Args:
value (Any): The value to be validated.
length (int): The length of the vector.
Raises:
TypeError: If the type of the argument is not a vector,
or the length of the vector is not equal to the given length.
"""
cls.validate_argument_type(value=value, typelist=[list, tuple])
if len(value) != length:
raise ValueError(f'Invalid vector length, expected 3 (got {len(value)} instead)')
for val in value:
if not isinstance(val, float) and not isinstance(val, int):
raise TypeError(
f"Invalid argument type, expected 'float' vector or 'int' vector (got '{val.__class__.__name__}' in vector instead)."
)
def get_variable_name(_type: Any) -> str:
"""Get the variable name as a string."""
try:
return _type.__name__
except AttributeError:
return str(_type)
def get_function_signature(func: Callable) -> str:
"""Get the function signature as a string."""
_signature = signature(func)
parameters = [f'{param.name}: {get_variable_name(param.annotation)}' for param in _signature.parameters.values()]
return f"def {func.__name__}({', '.join(parameters)}) -> {get_variable_name(_signature.return_annotation)}:"
# TODO: finish this decorator, right now not support typing quoted as string
def validate(func: Callable) -> Callable:
"""Decorator to validate the types of arguments and return value of a function."""
def wrapper(*args, **kwargs):
# convert kwargs arguments to positional
# https://stackoverflow.com/questions/33448997/convert-kwargs-arguments-to-positional
func_signature = signature(func)
bound_arguments: BoundArguments = func_signature.bind(*args, **kwargs)
bound_arguments.apply_defaults()
# Check the types of arguments before calling the decorated function
for arg_name, arg_type in func.__annotations__.items():
if arg_name == 'return':
continue
_arg = bound_arguments.arguments[arg_name]
# List or Tuple
if any([get_origin(arg_type) is _type for _type in [list, List, tuple, Tuple]]):
# is list or tuple
if not isinstance(_arg, (list, tuple)):
raise TypeError(
f'{get_function_signature(func)}\n'
f' Argument "{arg_name}" must be of type "{get_variable_name(arg_type)}", '
f'got `{arg_name}={_arg}` (type "{get_variable_name(_arg.__class__)}") instead'
)
# length for tuple
if get_origin(arg_type) is tuple and len(_arg) != len(get_args(arg_type)):
raise TypeError(
f'{get_function_signature(func)}\n'
f' Argument "{arg_name}" must be of type "{get_variable_name(arg_type)}" in length {len(get_args(arg_type))}, '
f'got `{arg_name}={_arg}` (length {len(_arg)}) instead'
)
# type
if not all([isinstance(_a, get_args(arg_type)) for _a in _arg]):
raise TypeError(
f'{get_function_signature(func)}\n'
f' Argument "{arg_name}" must be of type "{get_variable_name(arg_type)}", '
f'got `{arg_name}={_arg}` (type "{get_variable_name(_arg.__class__)}") instead'
)
# Union
elif get_origin(arg_type) is Union:
if not isinstance(_arg, get_args(arg_type)):
raise TypeError(
f'{get_function_signature(func)}\n'
f' Argument "{arg_name}" must be one of type '
f'{[get_variable_name(_type) for _type in get_args(arg_type)]}, '
f'got `{arg_name}={_arg}` (type "{get_variable_name(_arg.__class__)}") instead'
)
# Other types
elif not isinstance(_arg, arg_type):
raise TypeError(
f'{get_function_signature(func)}\n'
f' Argument "{arg_name}" must be of type "{get_variable_name(arg_type)}", '
f'got `{arg_name}={_arg}` (type "{get_variable_name(_arg.__class__)}") instead'
)
result = func(*args, **kwargs)
# Check the type of the return value after calling the decorated function
return_type = func.__annotations__.get('return')
if return_type is not None and not isinstance(result, return_type):
raise TypeError(
f'{get_function_signature(func)}\n'
f'Return value must be of type "{get_variable_name(return_type)}", '
f'got {result} (type "{get_variable_name(result.__class__)}") instead'
)
return result
return wrapper