docs for muutils v0.9.1
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
  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 typing.Literal:
126            return _return_func(value in args)
127
128        if origin is list:
129            # no args
130            if len(args) == 0:
131                return _return_func(isinstance(value, list))
132            # incorrect number of args
133            if len(args) != 1:
134                raise InvalidGenericAliasError(
135                    f"Too many arguments for list expected 1, got {args = },   {expected_type = },   {value = },   {origin = }",
136                    f"{GenericAliasTypes = }",
137                )
138            # check is list
139            if not isinstance(value, list):
140                return _return_func(False)
141            # check all items in list are of the correct type
142            item_type = args[0]
143            return _return_func(all(validate_type(item, item_type) for item in value))
144
145        if origin is dict:
146            # no args
147            if len(args) == 0:
148                return _return_func(isinstance(value, dict))
149            # incorrect number of args
150            if len(args) != 2:
151                raise InvalidGenericAliasError(
152                    f"Expected 2 arguments for dict, expected 2, got {args = },   {expected_type = },   {value = },   {origin = }",
153                    f"{GenericAliasTypes = }",
154                )
155            # check is dict
156            if not isinstance(value, dict):
157                return _return_func(False)
158            # check all items in dict are of the correct type
159            key_type: type = args[0]
160            value_type: type = args[1]
161            return _return_func(
162                all(
163                    validate_type(key, key_type) and validate_type(val, value_type)
164                    for key, val in value.items()
165                )
166            )
167
168        if origin is set:
169            # no args
170            if len(args) == 0:
171                return _return_func(isinstance(value, set))
172            # incorrect number of args
173            if len(args) != 1:
174                raise InvalidGenericAliasError(
175                    f"Expected 1 argument for Set, got {args = },   {expected_type = },   {value = },   {origin = }",
176                    f"{GenericAliasTypes = }",
177                )
178            # check is set
179            if not isinstance(value, set):
180                return _return_func(False)
181            # check all items in set are of the correct type
182            item_type = args[0]
183            return _return_func(all(validate_type(item, item_type) for item in value))
184
185        if origin is tuple:
186            # no args
187            if len(args) == 0:
188                return _return_func(isinstance(value, tuple))
189            # check is tuple
190            if not isinstance(value, tuple):
191                return _return_func(False)
192            # check correct number of items in tuple
193            if len(value) != len(args):
194                return _return_func(False)
195            # check all items in tuple are of the correct type
196            return _return_func(
197                all(validate_type(item, arg) for item, arg in zip(value, args))
198            )
199
200        if origin is type:
201            # no args
202            if len(args) == 0:
203                return _return_func(isinstance(value, type))
204            # incorrect number of args
205            if len(args) != 1:
206                raise InvalidGenericAliasError(
207                    f"Expected 1 argument for Type, got {args = },   {expected_type = },   {value = },   {origin = }",
208                    f"{GenericAliasTypes = }",
209                )
210            # check is type
211            item_type = args[0]
212            if item_type in value.__mro__:
213                return _return_func(True)
214            else:
215                return _return_func(False)
216
217        # TODO: Callables, etc.
218
219        raise TypeHintNotImplementedError(
220            f"Unsupported generic alias {expected_type = } for {value = },   {origin = },   {args = }",
221            f"{origin = }, {args = }",
222            f"\n{GenericAliasTypes = }",
223        )
224
225    else:
226        raise TypeHintNotImplementedError(
227            f"Unsupported type hint {expected_type = } for {value = }",
228            f"{origin = }, {args = }",
229            f"\n{GenericAliasTypes = }",
230        )
231
232
233def get_fn_allowed_kwargs(fn: typing.Callable[..., Any]) -> typing.Set[str]:
234    """Get the allowed kwargs for a function, raising an exception if the signature cannot be determined."""
235    try:
236        fn = unwrap(fn)
237        params = signature(fn).parameters
238    except ValueError as e:
239        fn_name: str = getattr(fn, "__name__", str(fn))
240        err_msg = f"Cannot retrieve signature for {fn_name = } {fn = }: {str(e)}"
241        raise ValueError(err_msg) from e
242
243    return {
244        param.name
245        for param in params.values()
246        if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY)
247    }

GenericAliasTypes: tuple[typing.Any, ...] = (<class 'types.GenericAlias'>, <class 'typing._GenericAlias'>, <class 'typing._UnionGenericAlias'>, <class 'typing._BaseGenericAlias'>)
class IncorrectTypeException(builtins.TypeError):
29class IncorrectTypeException(TypeError):
30    pass

Inappropriate argument type.

Inherited Members
builtins.TypeError
TypeError
builtins.BaseException
with_traceback
add_note
args
class TypeHintNotImplementedError(builtins.NotImplementedError):
33class TypeHintNotImplementedError(NotImplementedError):
34    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):
37class InvalidGenericAliasError(TypeError):
38    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:
 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 typing.Literal:
127            return _return_func(value in args)
128
129        if origin is list:
130            # no args
131            if len(args) == 0:
132                return _return_func(isinstance(value, list))
133            # incorrect number of args
134            if len(args) != 1:
135                raise InvalidGenericAliasError(
136                    f"Too many arguments for list expected 1, got {args = },   {expected_type = },   {value = },   {origin = }",
137                    f"{GenericAliasTypes = }",
138                )
139            # check is list
140            if not isinstance(value, list):
141                return _return_func(False)
142            # check all items in list are of the correct type
143            item_type = args[0]
144            return _return_func(all(validate_type(item, item_type) for item in value))
145
146        if origin is dict:
147            # no args
148            if len(args) == 0:
149                return _return_func(isinstance(value, dict))
150            # incorrect number of args
151            if len(args) != 2:
152                raise InvalidGenericAliasError(
153                    f"Expected 2 arguments for dict, expected 2, got {args = },   {expected_type = },   {value = },   {origin = }",
154                    f"{GenericAliasTypes = }",
155                )
156            # check is dict
157            if not isinstance(value, dict):
158                return _return_func(False)
159            # check all items in dict are of the correct type
160            key_type: type = args[0]
161            value_type: type = args[1]
162            return _return_func(
163                all(
164                    validate_type(key, key_type) and validate_type(val, value_type)
165                    for key, val in value.items()
166                )
167            )
168
169        if origin is set:
170            # no args
171            if len(args) == 0:
172                return _return_func(isinstance(value, set))
173            # incorrect number of args
174            if len(args) != 1:
175                raise InvalidGenericAliasError(
176                    f"Expected 1 argument for Set, got {args = },   {expected_type = },   {value = },   {origin = }",
177                    f"{GenericAliasTypes = }",
178                )
179            # check is set
180            if not isinstance(value, set):
181                return _return_func(False)
182            # check all items in set are of the correct type
183            item_type = args[0]
184            return _return_func(all(validate_type(item, item_type) for item in value))
185
186        if origin is tuple:
187            # no args
188            if len(args) == 0:
189                return _return_func(isinstance(value, tuple))
190            # check is tuple
191            if not isinstance(value, tuple):
192                return _return_func(False)
193            # check correct number of items in tuple
194            if len(value) != len(args):
195                return _return_func(False)
196            # check all items in tuple are of the correct type
197            return _return_func(
198                all(validate_type(item, arg) for item, arg in zip(value, args))
199            )
200
201        if origin is type:
202            # no args
203            if len(args) == 0:
204                return _return_func(isinstance(value, type))
205            # incorrect number of args
206            if len(args) != 1:
207                raise InvalidGenericAliasError(
208                    f"Expected 1 argument for Type, got {args = },   {expected_type = },   {value = },   {origin = }",
209                    f"{GenericAliasTypes = }",
210                )
211            # check is type
212            item_type = args[0]
213            if item_type in value.__mro__:
214                return _return_func(True)
215            else:
216                return _return_func(False)
217
218        # TODO: Callables, etc.
219
220        raise TypeHintNotImplementedError(
221            f"Unsupported generic alias {expected_type = } for {value = },   {origin = },   {args = }",
222            f"{origin = }, {args = }",
223            f"\n{GenericAliasTypes = }",
224        )
225
226    else:
227        raise TypeHintNotImplementedError(
228            f"Unsupported type hint {expected_type = } for {value = }",
229            f"{origin = }, {args = }",
230            f"\n{GenericAliasTypes = }",
231        )

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[..., Any]) -> Set[str]:
234def get_fn_allowed_kwargs(fn: typing.Callable[..., Any]) -> typing.Set[str]:
235    """Get the allowed kwargs for a function, raising an exception if the signature cannot be determined."""
236    try:
237        fn = unwrap(fn)
238        params = signature(fn).parameters
239    except ValueError as e:
240        fn_name: str = getattr(fn, "__name__", str(fn))
241        err_msg = f"Cannot retrieve signature for {fn_name = } {fn = }: {str(e)}"
242        raise ValueError(err_msg) from e
243
244    return {
245        param.name
246        for param in params.values()
247        if param.kind in (param.POSITIONAL_OR_KEYWORD, param.KEYWORD_ONLY)
248    }

Get the allowed kwargs for a function, raising an exception if the signature cannot be determined.