muutils.validate_type
experimental utility for validating types in python, see validate_type
1"""experimental utility for validating types in python, see `validate_type`""" 2 3from __future__ import annotations 4 5from inspect import signature, unwrap 6import types 7import typing 8import functools 9 10# this is also for python <3.10 compatibility 11_GenericAliasTypeNames: typing.List[str] = [ 12 "GenericAlias", 13 "_GenericAlias", 14 "_UnionGenericAlias", 15 "_BaseGenericAlias", 16] 17 18_GenericAliasTypesList: list = [ 19 getattr(typing, name, None) for name in _GenericAliasTypeNames 20] 21 22GenericAliasTypes: tuple = tuple([t for t in _GenericAliasTypesList if t is not None]) 23 24 25class IncorrectTypeException(TypeError): 26 pass 27 28 29class TypeHintNotImplementedError(NotImplementedError): 30 pass 31 32 33class InvalidGenericAliasError(TypeError): 34 pass 35 36 37def _return_validation_except( 38 return_val: bool, value: typing.Any, expected_type: typing.Any 39) -> bool: 40 if return_val: 41 return True 42 else: 43 raise IncorrectTypeException( 44 f"Expected {expected_type = } for {value = }", 45 f"{type(value) = }", 46 f"{type(value).__mro__ = }", 47 f"{typing.get_origin(expected_type) = }", 48 f"{typing.get_args(expected_type) = }", 49 "\ndo --tb=long in pytest to see full trace", 50 ) 51 return False 52 53 54def _return_validation_bool(return_val: bool) -> bool: 55 return return_val 56 57 58def validate_type( 59 value: typing.Any, expected_type: typing.Any, do_except: bool = False 60) -> bool: 61 """Validate that a `value` is of the `expected_type` 62 63 # Parameters 64 - `value`: the value to check the type of 65 - `expected_type`: the type to check against. Not all types are supported 66 - `do_except`: if `True`, raise an exception if the type is incorrect (instead of returning `False`) 67 (default: `False`) 68 69 # Returns 70 - `bool`: `True` if the value is of the expected type, `False` otherwise. 71 72 # Raises 73 - `IncorrectTypeException(TypeError)`: if the type is incorrect and `do_except` is `True` 74 - `TypeHintNotImplementedError(NotImplementedError)`: if the type hint is not implemented 75 - `InvalidGenericAliasError(TypeError)`: if the generic alias is invalid 76 77 use `typeguard` for a more robust solution: https://github.com/agronholm/typeguard 78 """ 79 if expected_type is typing.Any: 80 return True 81 82 # set up the return function depending on `do_except` 83 _return_func: typing.Callable[[bool], bool] = ( 84 # functools.partial doesn't hint the function signature 85 functools.partial( # type: ignore[assignment] 86 _return_validation_except, value=value, expected_type=expected_type 87 ) 88 if do_except 89 else _return_validation_bool 90 ) 91 92 # base type without args 93 if isinstance(expected_type, type): 94 try: 95 # if you use args on a type like `dict[str, int]`, this will fail 96 return _return_func(isinstance(value, expected_type)) 97 except TypeError as e: 98 if isinstance(e, IncorrectTypeException): 99 raise e 100 101 origin: typing.Any = typing.get_origin(expected_type) 102 args: tuple = typing.get_args(expected_type) 103 104 # useful for debugging 105 # print(f"{value = }, {expected_type = }, {origin = }, {args = }") 106 UnionType = getattr(types, "UnionType", None) 107 108 if (origin is typing.Union) or ( # this works in python <3.10 109 False 110 if UnionType is None # return False if UnionType is not available 111 else origin is UnionType # return True if UnionType is available 112 ): 113 return _return_func(any(validate_type(value, arg) for arg in args)) 114 115 # generic alias, more complicated 116 item_type: type 117 if isinstance(expected_type, GenericAliasTypes): 118 if origin is list: 119 # no args 120 if len(args) == 0: 121 return _return_func(isinstance(value, list)) 122 # incorrect number of args 123 if len(args) != 1: 124 raise InvalidGenericAliasError( 125 f"Too many arguments for list expected 1, got {args = }, {expected_type = }, {value = }, {origin = }", 126 f"{GenericAliasTypes = }", 127 ) 128 # check is list 129 if not isinstance(value, list): 130 return _return_func(False) 131 # check all items in list are of the correct type 132 item_type = args[0] 133 return all(validate_type(item, item_type) for item in value) 134 135 if origin is dict: 136 # no args 137 if len(args) == 0: 138 return _return_func(isinstance(value, dict)) 139 # incorrect number of args 140 if len(args) != 2: 141 raise InvalidGenericAliasError( 142 f"Expected 2 arguments for dict, expected 2, got {args = }, {expected_type = }, {value = }, {origin = }", 143 f"{GenericAliasTypes = }", 144 ) 145 # check is dict 146 if not isinstance(value, dict): 147 return _return_func(False) 148 # check all items in dict are of the correct type 149 key_type: type = args[0] 150 value_type: type = args[1] 151 return _return_func( 152 all( 153 validate_type(key, key_type) and validate_type(val, value_type) 154 for key, val in value.items() 155 ) 156 ) 157 158 if origin is set: 159 # no args 160 if len(args) == 0: 161 return _return_func(isinstance(value, set)) 162 # incorrect number of args 163 if len(args) != 1: 164 raise InvalidGenericAliasError( 165 f"Expected 1 argument for Set, got {args = }, {expected_type = }, {value = }, {origin = }", 166 f"{GenericAliasTypes = }", 167 ) 168 # check is set 169 if not isinstance(value, set): 170 return _return_func(False) 171 # check all items in set are of the correct type 172 item_type = args[0] 173 return _return_func(all(validate_type(item, item_type) for item in value)) 174 175 if origin is tuple: 176 # no args 177 if len(args) == 0: 178 return _return_func(isinstance(value, tuple)) 179 # check is tuple 180 if not isinstance(value, tuple): 181 return _return_func(False) 182 # check correct number of items in tuple 183 if len(value) != len(args): 184 return _return_func(False) 185 # check all items in tuple are of the correct type 186 return _return_func( 187 all(validate_type(item, arg) for item, arg in zip(value, args)) 188 ) 189 190 if origin is type: 191 # no args 192 if len(args) == 0: 193 return _return_func(isinstance(value, type)) 194 # incorrect number of args 195 if len(args) != 1: 196 raise InvalidGenericAliasError( 197 f"Expected 1 argument for Type, got {args = }, {expected_type = }, {value = }, {origin = }", 198 f"{GenericAliasTypes = }", 199 ) 200 # check is type 201 item_type = args[0] 202 if item_type in value.__mro__: 203 return _return_func(True) 204 else: 205 return _return_func(False) 206 207 # TODO: Callables, etc. 208 209 raise TypeHintNotImplementedError( 210 f"Unsupported generic alias {expected_type = } for {value = }, {origin = }, {args = }", 211 f"{origin = }, {args = }", 212 f"\n{GenericAliasTypes = }", 213 ) 214 215 else: 216 raise TypeHintNotImplementedError( 217 f"Unsupported type hint {expected_type = } for {value = }", 218 f"{origin = }, {args = }", 219 f"\n{GenericAliasTypes = }", 220 ) 221 222 223def get_fn_allowed_kwargs(fn: typing.Callable) -> typing.Set[str]: 224 """Get the allowed kwargs for a function, raising an exception if the signature cannot be determined.""" 225 try: 226 fn = unwrap(fn) 227 params = signature(fn).parameters 228 except ValueError as e: 229 raise ValueError( 230 f"Cannot retrieve signature for {fn.__name__ = } {fn = }: {str(e)}" 231 ) from e 232 233 return { 234 param.name 235 for param in params.values() 236 if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY) 237 }
GenericAliasTypes: tuple =
(<class 'types.GenericAlias'>, <class 'typing._GenericAlias'>, <class 'typing._UnionGenericAlias'>, <class 'typing._BaseGenericAlias'>)
class
IncorrectTypeException(builtins.TypeError):
Inappropriate argument type.
Inherited Members
- builtins.TypeError
- TypeError
- builtins.BaseException
- with_traceback
- add_note
- args
class
TypeHintNotImplementedError(builtins.NotImplementedError):
Method or function hasn't been implemented yet.
Inherited Members
- builtins.NotImplementedError
- NotImplementedError
- builtins.BaseException
- with_traceback
- add_note
- args
class
InvalidGenericAliasError(builtins.TypeError):
Inappropriate argument type.
Inherited Members
- builtins.TypeError
- TypeError
- builtins.BaseException
- with_traceback
- add_note
- args
def
validate_type(value: Any, expected_type: Any, do_except: bool = False) -> bool:
59def validate_type( 60 value: typing.Any, expected_type: typing.Any, do_except: bool = False 61) -> bool: 62 """Validate that a `value` is of the `expected_type` 63 64 # Parameters 65 - `value`: the value to check the type of 66 - `expected_type`: the type to check against. Not all types are supported 67 - `do_except`: if `True`, raise an exception if the type is incorrect (instead of returning `False`) 68 (default: `False`) 69 70 # Returns 71 - `bool`: `True` if the value is of the expected type, `False` otherwise. 72 73 # Raises 74 - `IncorrectTypeException(TypeError)`: if the type is incorrect and `do_except` is `True` 75 - `TypeHintNotImplementedError(NotImplementedError)`: if the type hint is not implemented 76 - `InvalidGenericAliasError(TypeError)`: if the generic alias is invalid 77 78 use `typeguard` for a more robust solution: https://github.com/agronholm/typeguard 79 """ 80 if expected_type is typing.Any: 81 return True 82 83 # set up the return function depending on `do_except` 84 _return_func: typing.Callable[[bool], bool] = ( 85 # functools.partial doesn't hint the function signature 86 functools.partial( # type: ignore[assignment] 87 _return_validation_except, value=value, expected_type=expected_type 88 ) 89 if do_except 90 else _return_validation_bool 91 ) 92 93 # base type without args 94 if isinstance(expected_type, type): 95 try: 96 # if you use args on a type like `dict[str, int]`, this will fail 97 return _return_func(isinstance(value, expected_type)) 98 except TypeError as e: 99 if isinstance(e, IncorrectTypeException): 100 raise e 101 102 origin: typing.Any = typing.get_origin(expected_type) 103 args: tuple = typing.get_args(expected_type) 104 105 # useful for debugging 106 # print(f"{value = }, {expected_type = }, {origin = }, {args = }") 107 UnionType = getattr(types, "UnionType", None) 108 109 if (origin is typing.Union) or ( # this works in python <3.10 110 False 111 if UnionType is None # return False if UnionType is not available 112 else origin is UnionType # return True if UnionType is available 113 ): 114 return _return_func(any(validate_type(value, arg) for arg in args)) 115 116 # generic alias, more complicated 117 item_type: type 118 if isinstance(expected_type, GenericAliasTypes): 119 if origin is list: 120 # no args 121 if len(args) == 0: 122 return _return_func(isinstance(value, list)) 123 # incorrect number of args 124 if len(args) != 1: 125 raise InvalidGenericAliasError( 126 f"Too many arguments for list expected 1, got {args = }, {expected_type = }, {value = }, {origin = }", 127 f"{GenericAliasTypes = }", 128 ) 129 # check is list 130 if not isinstance(value, list): 131 return _return_func(False) 132 # check all items in list are of the correct type 133 item_type = args[0] 134 return all(validate_type(item, item_type) for item in value) 135 136 if origin is dict: 137 # no args 138 if len(args) == 0: 139 return _return_func(isinstance(value, dict)) 140 # incorrect number of args 141 if len(args) != 2: 142 raise InvalidGenericAliasError( 143 f"Expected 2 arguments for dict, expected 2, got {args = }, {expected_type = }, {value = }, {origin = }", 144 f"{GenericAliasTypes = }", 145 ) 146 # check is dict 147 if not isinstance(value, dict): 148 return _return_func(False) 149 # check all items in dict are of the correct type 150 key_type: type = args[0] 151 value_type: type = args[1] 152 return _return_func( 153 all( 154 validate_type(key, key_type) and validate_type(val, value_type) 155 for key, val in value.items() 156 ) 157 ) 158 159 if origin is set: 160 # no args 161 if len(args) == 0: 162 return _return_func(isinstance(value, set)) 163 # incorrect number of args 164 if len(args) != 1: 165 raise InvalidGenericAliasError( 166 f"Expected 1 argument for Set, got {args = }, {expected_type = }, {value = }, {origin = }", 167 f"{GenericAliasTypes = }", 168 ) 169 # check is set 170 if not isinstance(value, set): 171 return _return_func(False) 172 # check all items in set are of the correct type 173 item_type = args[0] 174 return _return_func(all(validate_type(item, item_type) for item in value)) 175 176 if origin is tuple: 177 # no args 178 if len(args) == 0: 179 return _return_func(isinstance(value, tuple)) 180 # check is tuple 181 if not isinstance(value, tuple): 182 return _return_func(False) 183 # check correct number of items in tuple 184 if len(value) != len(args): 185 return _return_func(False) 186 # check all items in tuple are of the correct type 187 return _return_func( 188 all(validate_type(item, arg) for item, arg in zip(value, args)) 189 ) 190 191 if origin is type: 192 # no args 193 if len(args) == 0: 194 return _return_func(isinstance(value, type)) 195 # incorrect number of args 196 if len(args) != 1: 197 raise InvalidGenericAliasError( 198 f"Expected 1 argument for Type, got {args = }, {expected_type = }, {value = }, {origin = }", 199 f"{GenericAliasTypes = }", 200 ) 201 # check is type 202 item_type = args[0] 203 if item_type in value.__mro__: 204 return _return_func(True) 205 else: 206 return _return_func(False) 207 208 # TODO: Callables, etc. 209 210 raise TypeHintNotImplementedError( 211 f"Unsupported generic alias {expected_type = } for {value = }, {origin = }, {args = }", 212 f"{origin = }, {args = }", 213 f"\n{GenericAliasTypes = }", 214 ) 215 216 else: 217 raise TypeHintNotImplementedError( 218 f"Unsupported type hint {expected_type = } for {value = }", 219 f"{origin = }, {args = }", 220 f"\n{GenericAliasTypes = }", 221 )
Validate that a value
is of the expected_type
Parameters
value
: the value to check the type ofexpected_type
: the type to check against. Not all types are supporteddo_except
: ifTrue
, raise an exception if the type is incorrect (instead of returningFalse
) (default:False
)
Returns
bool
:True
if the value is of the expected type,False
otherwise.
Raises
IncorrectTypeException(TypeError)
: if the type is incorrect anddo_except
isTrue
TypeHintNotImplementedError(NotImplementedError)
: if the type hint is not implementedInvalidGenericAliasError(TypeError)
: if the generic alias is invalid
use typeguard
for a more robust solution: https://github.com/agronholm/typeguard
def
get_fn_allowed_kwargs(fn: Callable) -> Set[str]:
224def get_fn_allowed_kwargs(fn: typing.Callable) -> typing.Set[str]: 225 """Get the allowed kwargs for a function, raising an exception if the signature cannot be determined.""" 226 try: 227 fn = unwrap(fn) 228 params = signature(fn).parameters 229 except ValueError as e: 230 raise ValueError( 231 f"Cannot retrieve signature for {fn.__name__ = } {fn = }: {str(e)}" 232 ) from e 233 234 return { 235 param.name 236 for param in params.values() 237 if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY) 238 }
Get the allowed kwargs for a function, raising an exception if the signature cannot be determined.