docs for muutils v0.6.17
View Source on GitHub

# muutils.validate_type

experimental utility for validating types in python, see `validate_type`

View Source
View on GitHub

      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):
View Source
View on GitHub

    25class IncorrectTypeException(TypeError):
    26    pass

Inappropriate argument type.

##### Inherited Members

builtins.TypeError
  TypeError
builtins.BaseException
  with_traceback
  add_note
  args

class TypeHintNotImplementedError(builtins.NotImplementedError):
View Source
View on GitHub

    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):
View Source
View on GitHub

    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:
View Source
View on GitHub

     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