docs for muutils v0.6.16
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
  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):
25class IncorrectTypeException(TypeError):
26    pass

Inappropriate argument type.

Inherited Members
builtins.TypeError
TypeError
builtins.BaseException
with_traceback
add_note
args
class TypeHintNotImplementedError(builtins.NotImplementedError):
29class TypeHintNotImplementedError(NotImplementedError):
30    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):
33class InvalidGenericAliasError(TypeError):
34    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:
 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 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