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