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