docs for muutils v0.8.3
View Source on GitHub

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):
26class IncorrectTypeException(TypeError):
27    pass

Inappropriate argument type.

Inherited Members
builtins.TypeError
TypeError
builtins.BaseException
with_traceback
add_note
args
class TypeHintNotImplementedError(builtins.NotImplementedError):
30class TypeHintNotImplementedError(NotImplementedError):
31    pass

Method or function hasn't been implemented yet.

Inherited Members
builtins.NotImplementedError
NotImplementedError
builtins.BaseException
with_traceback
add_note
args
class InvalidGenericAliasError(builtins.TypeError):
34class InvalidGenericAliasError(TypeError):
35    pass

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 of
  • expected_type: the type to check against. Not all types are supported
  • do_except: if True, raise an exception if the type is incorrect (instead of returning False) (default: False)

Returns

  • bool: True if the value is of the expected type, False otherwise.

Raises

  • IncorrectTypeException(TypeError): if the type is incorrect and do_except is True
  • TypeHintNotImplementedError(NotImplementedError): if the type hint is not implemented
  • InvalidGenericAliasError(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.