Source code for typing_validation.validation

"""
    Core type validation functionality.
"""

from __future__ import annotations

from contextlib import contextmanager
import collections
import collections.abc as collections_abc
from keyword import iskeyword
import sys
import typing
from typing import (
    Any,
    ForwardRef,
    Hashable,
    Optional,
    TypeVar,
    Union,
    get_type_hints,
)

from .validation_failure import (
    InvalidNumpyDTypeValidationFailure,
    SubtypeValidationFailure,
    TypeVarBoundValidationFailure,
    ValidationFailureAtIdx,
    ValidationFailureAtKey,
    MissingKeysValidationFailure,
    UnionValidationFailure,
    ValidationFailure,
    _set_latest_validation_failure,
)
from .inspector import TypeInspector

if sys.version_info[1] >= 8:
    from typing import Literal, Protocol
else:
    from typing_extensions import Literal, Protocol

if sys.version_info[1] >= 9:
    from keyword import issoftkeyword
else:

    def issoftkeyword(s: str) -> bool:
        r"""Dummy implementation for issoftkeyword in Python 3.7 and 3.8."""
        return s == "_"


if sys.version_info[1] >= 10:
    from types import NoneType, UnionType
else:
    NoneType = type(None)
    UnionType = None


try:
    import typing_extensions
except ModuleNotFoundError:
    _typing_modules = [typing]
else:
    _typing_modules = [typing, typing_extensions]


_validation_aliases: typing.Dict[str, Any] = {}
r"""
    Current context of type aliases, used to resolve forward references to type aliases in :func:`validate`.
"""


[docs] @contextmanager def validation_aliases(**aliases: Any) -> collections.abc.Iterator[None]: r""" Sets type aliases that can be used to resolve forward references in :func:`validate`. For example, the following snippet validates a value against a recursive type alias for JSON-like objects, using :func:`validation_aliases` to create a context where :func:`validate` internally evaluates the forward reference ``"JSON"`` to the type alias ``JSON``: >>> JSON = Union[int, float, bool, None, str, list["JSON"], dict[str, "JSON"]] >>> with validation_aliases(JSON=JSON): >>> validate([1, 2.2, {"a": ["Hello", None, {"b": True}]}], list["JSON"]) """ # pylint: disable = global-statement global _validation_aliases outer_validation_aliases = _validation_aliases _validation_aliases = {**_validation_aliases} _validation_aliases.update(aliases) try: yield finally: _validation_aliases = outer_validation_aliases
def _get_type_classes(name: str) -> typing.List[typing.Type[Any]]: """Get the classes for the specified type from typing and its possible backport modules.""" return [ getattr(module, name) for module in _typing_modules if hasattr(module, name) ] # basic types _basic_types = frozenset( {bool, int, float, complex, bytes, bytearray, memoryview, str, range, slice} ) # collection types (parametric on item type) _collection_pseudotypes_dict = { typing.Collection: collections_abc.Collection, typing.AbstractSet: collections_abc.Set, typing.MutableSet: collections_abc.MutableSet, typing.Sequence: collections_abc.Sequence, typing.MutableSequence: collections_abc.MutableSequence, typing.Deque: collections.deque, typing.List: list, typing.Set: set, typing.FrozenSet: frozenset, } _collection_pseudotypes = frozenset(_collection_pseudotypes_dict.keys()) | frozenset( _collection_pseudotypes_dict.values() ) _collection_origins = frozenset(_collection_pseudotypes_dict.values()) # ordered collection types (parametric on item type) _ordered_collection_pseudotypes_dict = { typing.Sequence: collections_abc.Sequence, typing.MutableSequence: collections_abc.MutableSequence, typing.Deque: collections.deque, typing.List: list, } _ordered_collection_pseudotypes = frozenset( _ordered_collection_pseudotypes_dict.keys() ) | frozenset(_ordered_collection_pseudotypes_dict.values()) _ordered_collection_origins = frozenset(_ordered_collection_pseudotypes_dict.values()) # types that might be validated as collections (parametric on item type) _maybe_collection_pseudotypes_dict = { typing.Iterable: collections_abc.Iterable, typing.Container: collections_abc.Container, } _maybe_collection_pseudotypes = frozenset( _maybe_collection_pseudotypes_dict.keys() ) | frozenset(_maybe_collection_pseudotypes_dict.values()) _maybe_collection_origins = frozenset(_maybe_collection_pseudotypes_dict.values()) # mapping types (parametric on both key type and value type) _mapping_pseudotypes_dict = { typing.Mapping: collections_abc.Mapping, typing.MutableMapping: collections_abc.MutableMapping, typing.Dict: dict, typing.DefaultDict: collections.defaultdict, } _mapping_pseudotypes = frozenset(_mapping_pseudotypes_dict.keys()) | frozenset( _mapping_pseudotypes_dict.values() ) _mapping_origins = frozenset(_mapping_pseudotypes_dict.values()) # tuple and namedtuples _tuple_pseudotypes = frozenset( {typing.Tuple, tuple, typing.NamedTuple, collections.namedtuple} ) _tuple_origins = frozenset({tuple, collections.namedtuple}) # other types _other_pseudotypes_dict = { typing.Iterator: collections_abc.Iterator, typing.Hashable: collections_abc.Hashable, typing.Sized: collections_abc.Sized, } if sys.version_info[1] <= 11: _other_pseudotypes_dict[typing.ByteString] = collections_abc.ByteString # type: ignore else: from collections.abc import Buffer as _collections_abc_Buffer _other_pseudotypes_dict[_collections_abc_Buffer] = _collections_abc_Buffer _other_pseudotypes = frozenset(_other_pseudotypes_dict.keys()) | frozenset( _other_pseudotypes_dict.values() ) _other_origins = frozenset(_other_pseudotypes_dict.values()) _iterator_origins = frozenset( [ typing.Iterator, collections_abc.Iterator, typing.Iterable, collections_abc.Iterable, ] ) # all types together _pseudotypes_dict: typing.Mapping[Any, Any] = { **_collection_pseudotypes_dict, **_maybe_collection_pseudotypes_dict, **_mapping_pseudotypes_dict, **_other_pseudotypes_dict, } # used by tests _pseudotypes = ( _collection_pseudotypes | _maybe_collection_pseudotypes | _mapping_pseudotypes | _tuple_pseudotypes | _other_pseudotypes ) _origins = ( _collection_origins | _maybe_collection_origins | _mapping_origins | _tuple_origins | _other_origins )
[docs] class UnsupportedTypeError(ValueError): """ Class for errors raised when attempting to validate an unsupported type. .. warning:: Currently extends :obj:`ValueError` for backwards compatibility. This will be changed to :obj:`NotImplementedError` in v1.3.0. """
def _unsupported_type_error( t: Any, explanation: Union[str, None] = None ) -> UnsupportedTypeError: """ Error for unsupported types, with optional explanation. """ msg = f"Unsupported validation for type {t!r}." if explanation is not None: msg += " " + explanation return UnsupportedTypeError(msg) def _type_error( val: Any, t: Any, *errors: TypeError, is_union: bool = False ) -> TypeError: """ Type error arising from ``val`` not being an instance of type ``t``. If other type errors are passed as causes, their error messages are indented and included. A :func:`validation_failure` attribute of type ValidationFailure is set for the error, including full information about the chain of validation failures. """ causes: typing.Tuple[ValidationFailure, ...] = tuple( getattr(error, "validation_failure") for error in errors if hasattr(error, "validation_failure") ) assert all(isinstance(cause, ValidationFailure) for cause in causes) validation_failure: ValidationFailure if is_union: validation_failure = UnionValidationFailure( val, t, *causes, type_aliases=_validation_aliases ) else: validation_failure = ValidationFailure( val, t, *causes, type_aliases=_validation_aliases ) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _typevar_error(val: Any, t: Any, bound_error: TypeError) -> TypeError: assert hasattr(bound_error, "validation_failure"), bound_error cause = getattr(bound_error, "validation_failure") assert isinstance(cause, ValidationFailure), cause validation_failure = TypeVarBoundValidationFailure(val, t, cause) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _idx_type_error( val: Any, t: Any, idx_error: TypeError, *, idx: int, ordered: bool ) -> TypeError: assert hasattr(idx_error, "validation_failure"), idx_error idx_cause = getattr(idx_error, "validation_failure") assert isinstance(idx_cause, ValidationFailure), idx_cause validation_failure = ValidationFailureAtIdx( val, t, idx_cause, idx=idx, ordered=ordered ) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _key_type_error(val: Any, t: Any, key_error: TypeError, *, key: Any) -> TypeError: assert hasattr(key_error, "validation_failure"), key_error key_cause = getattr(key_error, "validation_failure") assert isinstance(key_cause, ValidationFailure), key_cause validation_failure = ValidationFailureAtKey(val, t, key_cause, key=key) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _missing_keys_type_error(val: Any, t: Any, *missing_keys: Any) -> TypeError: validation_failure = MissingKeysValidationFailure(val, t, missing_keys) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _subtype_error(s: Any, t: Any) -> TypeError: validation_failure = SubtypeValidationFailure(s, t) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error def _type_alias_error(t_alias: str, cause: TypeError) -> TypeError: """ Repackages a validation error as a type alias error. """ assert hasattr(cause, "validation_failure"), cause validation_failure = getattr(cause, "validation_failure") assert isinstance(validation_failure, ValidationFailure), validation_failure validation_failure._t = t_alias return cause def _numpy_dtype_error(val: Any, t: Any) -> TypeError: """ Type error arising from ``val`` not being an instance of NumPy array type ``t``, because ``val.dtype`` is not valid. """ validation_failure = InvalidNumpyDTypeValidationFailure(val, t) error = TypeError(str(validation_failure)) setattr(error, "validation_failure", validation_failure) return error # def _missing_args_msg(t: Any) -> str: # """Error message for missing :attr:`__args__` attribute on a type ``t``.""" # return f"For type {repr(t)}, expected '__args__' attribute." # pragma: nocover # def _wrong_args_num_msg(t: Any, num_args: int) -> str: # """Error message for incorrect number of :attr:`__args__` on a type ``t``.""" # return f"For type {repr(t)}, expected '__args__' to be tuple with {num_args} elements." # pragma: nocover def _validate_type(val: Any, t: type) -> None: """Basic validation using :func:`isinstance`""" if isinstance(val, TypeInspector): val._record_type(t) return if not isinstance(val, t): raise _type_error(val, t) def _validate_collection(val: Any, t: Any, ordered: bool) -> None: """Parametric collection validation (i.e. recursive validation of all items).""" # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert isinstance(t__args__, tuple) and len(t__args__) == 1, _wrong_args_num_msg( # t, 1 # ) item_t = t__args__[0] if isinstance(val, TypeInspector): val._record_collection(item_t) validate(val, item_t) return for idx, item in enumerate(val): try: validate(item, item_t) except TypeError as e: raise _idx_type_error(val, t, e, idx=idx, ordered=ordered) from None def _validate_mapping(val: Any, t: Any) -> None: """Parametric mapping validation (i.e. recursive validation of all keys and values).""" # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert isinstance(t__args__, tuple) and len(t__args__) == 2, _wrong_args_num_msg( # t, 2 # ) key_t, value_t = t__args__ if isinstance(val, TypeInspector): val._record_mapping(key_t, value_t) validate(val, key_t) validate(val, value_t) return for key, value in val.items(): try: validate(key, key_t) except TypeError as e: raise _type_error(val, t, e) from None try: validate(value, value_t) except TypeError as e: raise _key_type_error(val, t, e, key=key) from None def _validate_tuple(val: Any, t: Any) -> None: """ Parametric tuple validation (i.e. recursive validation of all items). Two cases: - variadic tuple types: arbitrary number of items, all of same type - fixed-length tuple types: fixed number of items, each with its individual type """ # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert isinstance( # t__args__, tuple # ), f"For type {repr(t)}, expected '__args__' to be a tuple." if ... in t__args__: # variadic tuple # assert len(t__args__) == 2, _wrong_args_num_msg(t, 2) item_t = t__args__[0] if isinstance(val, TypeInspector): val._record_variadic_tuple(item_t) validate(val, item_t) return for idx, item in enumerate(val): try: validate(item, item_t) except TypeError as e: raise _idx_type_error(val, t, e, idx=idx, ordered=True) from None else: # fixed-length tuple if isinstance(val, TypeInspector): val._record_fixed_tuple(*t__args__) for item_t in t__args__: validate(val, item_t) return if len(val) != len(t__args__): raise _type_error(val, t) for idx, (item_t, item) in enumerate(zip(t__args__, val)): try: validate(item, item_t) except TypeError as e: raise _idx_type_error(val, t, e, idx=idx, ordered=True) from None def _validate_union(val: Any, t: Any, *, use_UnionType: bool = False) -> None: """ Union type validation. Each type ``u`` listed in the union type ``t`` is checked: - if ``val`` is an instance of ``t``, returns immediately without error - otherwise, moves to the next ``u`` If ``val`` is not an instance of any of the types listed in the union, type error is raised. """ # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert isinstance( # t__args__, tuple # ), f"For type {repr(t)}, expected '__args__' to be a tuple." if isinstance(val, TypeInspector): val._record_union(*t__args__, use_UnionType=use_UnionType) for member_t in t__args__: validate(val, member_t) return if not t__args__: return member_errors: typing.List[TypeError] = [] for member_t in t__args__: try: validate(val, member_t) return except TypeError as e: member_errors.append(e) raise _type_error(val, t, *member_errors, is_union=True) def _validate_literal(val: Any, t: Any) -> None: """ Literal type validation. """ # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert isinstance( # t__args__, tuple # ), f"For type {repr(t)}, expected '__args__' to be a tuple." if isinstance(val, TypeInspector): val._record_literal(*t__args__) return if val not in t__args__: raise _type_error(val, t) def _validate_alias(val: Any, t_alias: str) -> None: r""" Validation of type aliases within the context provided by :func:`validation` """ t = _validation_aliases[t_alias] if isinstance(val, TypeInspector): val._record_alias(t_alias) return nested_error: Optional[TypeError] = None try: validate(val, t) except TypeError as e: nested_error = e if nested_error is not None: raise _type_alias_error(t_alias, nested_error) def _is_typed_dict(t: type) -> bool: """ Determines whether a type is a subclass of :class:`TypedDict`. """ return t.__class__ in _get_type_classes("_TypedDictMeta") def _validate_typed_dict(val: Any, t: type) -> None: """ Validation of :class:`TypedDict` subclasses. """ annotations = get_type_hints(t) required_keys: frozenset[str] = getattr(t, "__required_keys__") if isinstance(val, TypeInspector): val._record_typed_dict(t) for k, val_t in annotations.items(): validate(val, val_t) return # 1. Validate that `val`` is a mapping with string keys: try: validate(val, typing.Mapping[str, typing.Any]) except TypeError as e: raise _type_error(val, t, e) from None # 2. Validate presence of required keys: missing_keys = [k for k in required_keys if k not in val] if missing_keys: raise _missing_keys_type_error(val, t, *missing_keys) # 3. Validate value types: for k, v in annotations.items(): if k in val: try: validate(val[k], v) except TypeError as e: raise _key_type_error(val, t, e, key=k) from None def _validate_user_class(val: Any, t: Any) -> None: # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ t__origin__ = t.__origin__ # assert isinstance( # t__args__, tuple # ), f"For type {repr(t)}, expected '__args__' to be a tuple." if isinstance(val, TypeInspector): if t__origin__ is type: if len(t__args__) != 1 or not _can_validate_subtype_of(t__args__[0]): val._record_unsupported_type(t) return val._record_pending_type_generic(t__origin__) val._record_user_class(*t__args__) for arg in t__args__: validate(val, arg) return _validate_type(val, t__origin__) if t__origin__ is type: if len(t__args__) != 1: raise _unsupported_type_error(t) _validate_subtype_of(val, t__args__[0]) return # TODO: Generic type arguments cannot be validated in general, # but in a future release it will be possible for classes to define # a dunder classmethod which can be used to validate type arguments. def __extract_member_types(u: Any) -> tuple[Any, ...] | None: q = collections.deque([u]) member_types: list[Any] = [] while q: t = q.popleft() if t is Any: return None elif UnionType is not None and isinstance(t, UnionType): q.extend(t.__args__) elif hasattr(t, "__origin__") and t.__origin__ is Union: q.extend(t.__args__) else: member_types.append(t) return tuple(member_types) def __check_can_validate_subtypes(*subtypes: Any) -> None: for s in subtypes: if not isinstance(s, type): raise ValueError( "validate(s, Type[t]) is only supported when 's' is " "an instance of 'type' or a union of instances of 'type'.\n" f"Found s = {'|'.join(str(s) for s in subtypes)}" ) def __check_can_validate_supertypes(*supertypes: Any) -> None: for t in supertypes: if not isinstance(t, type): raise ValueError( "validate(s, Type[t]) is only supported when 't' is " "an instance of 'type' or a union of instances of 'type'.\n" f"Found t = {'|'.join(str(t) for t in supertypes)}" ) def _can_validate_subtype_of(t: Any) -> bool: try: # This is the validation part of _validate_subtype: t_member_types = __extract_member_types(t) if t_member_types is not None: __check_can_validate_supertypes(*t_member_types) return True except ValueError: return False def _validate_subtype_of(s: Any, t: Any) -> None: # 1. Validation: __check_can_validate_subtypes(s) t_member_types = __extract_member_types(t) if t_member_types is None: # An Any was found amongst the member types, all good. return __check_can_validate_supertypes(*t_member_types) # 2. Subtype check: if not issubclass(s, t_member_types): raise _subtype_error(s, t) # TODO: improve support for subtype checks. def _extract_dtypes(t: Any) -> typing.Sequence[Any]: if t is Any: return [Any] if ( UnionType is not None and isinstance(t, UnionType) or hasattr(t, "__origin__") and t.__origin__ is Union ): return [dtype for member in t.__args__ for dtype in _extract_dtypes(member)] import numpy as np # pylint: disable = import-outside-toplevel if hasattr(t, "__origin__"): t__origin__ = t.__origin__ if t__origin__ in { np.number, np.inexact, np.floating, np.complexfloating, np.integer, np.signedinteger, np.unsignedinteger, }: if t == t__origin__[Any]: return [t__origin__] # TODO: add broader support for np.NBitBase subtypes if isinstance(t, type) and issubclass(t, np.generic): return [t] raise TypeError() def _validate_numpy_array(val: Any, t: Any) -> None: import numpy as np # pylint: disable = import-outside-toplevel if not isinstance(val, TypeInspector): _validate_type(val, np.ndarray) # assert hasattr(t, "__args__"), _missing_args_msg(t) t__args__ = t.__args__ # assert len(t.__args__) == 2, _wrong_args_num_msg(t, 2) dtype_t_container = t__args__[1] # assert hasattr(dtype_t_container, "__args__"), _missing_args_msg(dtype_t_container) # assert len(dtype_t_container.__args__) == 1, _wrong_args_num_msg( # dtype_t_container, 1 # ) dtype_t = dtype_t_container.__args__[0] try: dtypes = _extract_dtypes(dtype_t) except TypeError: if isinstance(val, TypeInspector): val._record_unsupported_type(t) return raise _unsupported_type_error( t, f"Unsupported NumPy dtype {dtype_t!r}." ) from None if isinstance(val, TypeInspector): val._record_pending_type_generic(t.__origin__) val._record_user_class(*t__args__) for arg in t__args__: validate(val, arg) return val_dtype = val.dtype if not any(dtype is Any or np.issubdtype(val_dtype, dtype) for dtype in dtypes): raise _numpy_dtype_error(val, t) validate(val.shape, t__args__[0]) return def _validate_typevar(val: Any, t: TypeVar) -> None: if isinstance(val, TypeInspector): val._record_typevar(t) pass bound = t.__bound__ if bound is not None: try: validate(val, bound) except TypeError as e: raise _typevar_error(val, t, e) from None # def _validate_callable(val: Any, t: Any) -> None: # """ # Callable validation # """ # assert hasattr(t, "__args__"), _missing_args_msg(t) # assert isinstance(t.__args__, tuple), f"For type {repr(t)}, expected '__args__' to be a tuple." # if not callable(val): # raise _type_error(val, t, is_union=True) # if not t.__args__: # return # exp_params = t.__args__[:-1] # exp_ret = t.__args__[-1] # sig = inspect.signature(val) # empty = sig.empty # params = sig.parameters # ret = sig.return_annotation # positional_only: typing.List[inspect.Parameter] = [] # positional_or_keyword: typing.Dict[str, inspect.Parameter] = {} # var_positional: Optional[inspect.Parameter] = None # keyword_only: typing.Dict[str, inspect.Parameter] = {} # var_keyword: Optional[inspect.Parameter] = None # for param_name, param in params.items(): # if param.kind == param.POSITIONAL_ONLY: # positional_only.append(param) # elif param.kind == param.POSITIONAL_OR_KEYWORD: # positional_or_keyword[param_name] = param # elif param.kind == param.VAR_POSITIONAL: # var_positional = param # elif param.kind == param.KEYWORD_ONLY: # keyword_only[param_name] = param # elif param.kind == param.VAR_KEYWORD: # var_keyword = param # # still work in progress # raise _type_error(val, t, is_union=True)
[docs] def validate(val: Any, t: Any) -> Literal[True]: """ Performs runtime type-checking for the value ``val`` against type ``t``. The function raises :obj:`TypeError` upon failure and returns :obj:`True` upon success. The :obj:`True` return value means that :func:`validate` can be gated behind assertions and compiled away on optimised execution: .. code-block:: python assert validate(val, t) # compiled away using -O and -OO For structured types, the error message keeps track of the chain of validation failures, e.g. >>> from typing import * >>> from typing_validation import validate >>> validate([[0, 1, 2], {"hi": 0}], list[Union[Collection[int], dict[str, str]]]) TypeError: Runtime validation error raised by validate(val, t), details below. For type list[typing.Union[typing.Collection[int], dict[str, str]]], invalid value at idx: 1 For union type typing.Union[typing.Collection[int], dict[str, str]], invalid value: {'hi': 0} For member type typing.Collection[int], invalid value at idx: 0 For type <class 'int'>, invalid value: 'hi' For member type dict[str, str], invalid value at key: 'hi' For type <class 'str'>, invalid value: 0 **Note.** For Python 3.7 and 3.8, use :obj:`~typing.Dict` and :obj:`~typing.List` instead of :obj:`dict` and :obj:`list` for the above examples. :param val: the value to be type-checked :type val: :obj:`~typing.Any` :param t: the type to type-check against :type t: :obj:`~typing.Any` :raises TypeError: if ``val`` is not of type ``t`` :raises UnsupportedTypeError: if validation for type ``t`` is not supported :raises AssertionError: if things go unexpectedly wrong with ``__args__`` for parametric types """ # pylint: disable = too-many-return-statements, too-many-branches, too-many-statements unsupported_type_error: Optional[UnsupportedTypeError] = None if not isinstance(t, Hashable): if isinstance(val, TypeInspector): val._record_unsupported_type(t) return True if unsupported_type_error is None: unsupported_type_error = _unsupported_type_error( t, "Type is not hashable." ) # pragma: nocover raise unsupported_type_error if t is typing.Type: # Replace non-generic 'Type' with non-generic 'type': t = type if t in _basic_types: # speed things up for the likely most common case _validate_type(val, typing.cast(type, t)) return True if t is None or t is NoneType: if isinstance(val, TypeInspector): val._record_none() return True if val is not None: raise _type_error(val, t) return True if t in _pseudotypes: _validate_type(val, typing.cast(type, t)) return True if t is Any: if isinstance(val, TypeInspector): val._record_any() return True return True if isinstance(t, TypeVar): _validate_typevar(val, t) return True if UnionType is not None and isinstance(t, UnionType): _validate_union(val, t, use_UnionType=True) return True if hasattr(t, "__origin__"): # parametric types t__origin__ = t.__origin__ if t__origin__ is Union: _validate_union(val, t) return True if t__origin__ in _get_type_classes("Literal"): _validate_literal(val, t) return True if t__origin__ in _origins: if isinstance(val, TypeInspector): val._record_pending_type_generic(t__origin__) else: _validate_type(val, t__origin__) if t__origin__ in _collection_origins: ordered = t__origin__ in _ordered_collection_origins _validate_collection(val, t, ordered) return True if t__origin__ in _mapping_origins: _validate_mapping(val, t) return True if t__origin__ is tuple: _validate_tuple(val, t) return True if t__origin__ in _iterator_origins: if isinstance(val, TypeInspector): _validate_collection(val, t, ordered=False) # Item type cannot be validated for iterators (use validated_iter) return True if t__origin__ in _maybe_collection_origins and isinstance( val, typing.Collection ): _validate_collection(val, t, ordered=False) return True elif isinstance(t__origin__, type): try: import numpy as np # pylint: disable = import-outside-toplevel if issubclass(t__origin__, np.ndarray): _validate_numpy_array(val, t) return True except ModuleNotFoundError: pass _validate_user_class(val, t) return True elif isinstance(t, type): # The `isinstance(t, type)` case goes after the `hasattr(t, "__origin__")` case: # e.g. `isinstance(list[int], type)` in 3.10, but we want to validate `list[int]` # as a parametric type, not merely as `list` (which is what `_validate_type` does). if Protocol in t.__mro__: # type: ignore[comparison-overlap] if hasattr(t, "_is_runtime_protocol") and getattr( t, "_is_runtime_protocol" ): _validate_type(val, t) return True if isinstance(val, TypeInspector): val._record_unsupported_type(t) return True unsupported_type_error = _unsupported_type_error( t, "Protocol class is not runtime-checkable." ) # pragma: nocover elif _is_typed_dict(t): _validate_typed_dict(val, t) return True else: _validate_type(val, t) return True elif isinstance(t, (str, ForwardRef)): if isinstance(t, str): t_alias: str = t else: t_alias = t.__forward_arg__ if t_alias not in _validation_aliases: if ( t_alias.isidentifier() and not iskeyword(t_alias) and not issoftkeyword(t_alias) ): hint = f"Perhaps set it with validation_aliases({t_alias}=...)?" else: hint = ( f"Perhaps set it with validation_aliases(**{{'{t_alias}': ...}})?" ) unsupported_type_error = _unsupported_type_error( t_alias, f"Type alias is not known. {hint}" ) # pragma: nocover else: _validate_alias(val, t_alias) return True if isinstance(val, TypeInspector): val._record_unsupported_type(t) return True if unsupported_type_error is None: unsupported_type_error = _unsupported_type_error(t) # pragma: nocover raise unsupported_type_error
[docs] def can_validate(t: Any) -> TypeInspector: """ Checks whether validation is supported for the given type ``t``: if not, :func:`validate` will raise :obj:`UnsupportedTypeError`. .. warning:: The return type will be changed to :obj:`bool` in v1.3.0. To obtain a :class:`TypeInspector` object, please use the newly introduced :func:`inspect_type` instead. :param t: the type to be checked for validation support :type t: :obj:`~typing.Any` """ inspector = TypeInspector() validate(inspector, t) return inspector
[docs] def inspect_type(t: Any) -> TypeInspector: r""" Returns a :class:`TypeInspector` instance can be used wherever a boolean is expected, and will indicate whether the type is supported or not: >>> from typing import * >>> from typing_validation import inspect_type >>> res = inspect_type(tuple[list[str], Union[int, float, Callable[[int], int]]]) >>> bool(res) False The instance also records (with minimal added cost) the full structure of the type as the latter was validated, which it then exposes via its :attr:`TypeInspector.recorded_type` property: >>> res = inspect_type(tuple[list[Union[str, int]],...]) >>> bool(res) True >>> res.recorded_type tuple[list[typing.Union[str, int]], ...] Any unsupported subtype encountered during the validation is left in place, wrapped into an :class:`UnsupportedType`: >>> inspect_type(tuple[list[str], Union[int, float, Callable[[int], int]]]) The following type cannot be validated against: tuple[ list[ str ], Union[ int, float, UnsupportedType[ typing.Callable[[int], int] ], ], ] **Note.** For Python 3.7 and 3.8, use :obj:`~typing.Tuple` and :obj:`~typing.List` instead of :obj:`tuple` and :obj:`list` for the above examples. :param t: the type to be checked for validation support :type t: :obj:`~typing.Any` """ inspector = TypeInspector() validate(inspector, t) return inspector
T = typing.TypeVar("T") """ Invariant type variable used by the functions :func:`validated` and :func:`validated_iter`. """
[docs] def is_valid(val: T, t: Any) -> bool: """ Performs the same functionality as :func:`validate`, but returning :obj:`False` if validation is unsuccessful instead of raising error. In case of validation failure, detailed failure information is accessible via :func:`~typing_validation.validation_failure.latest_validation_failure`. """ try: validate(val, t) _set_latest_validation_failure(None) return True except TypeError as e: _set_latest_validation_failure(getattr(e, "validation_failure")) return False
[docs] def validated(val: T, t: Any) -> T: """ Performs the same functionality as :func:`validate`, but returns ``val`` if validation is successful. Useful when multiple elements must be validated as part of a larger expression, e.g. as part of a comprehension: .. code-block :: python def sortint(*items: int) -> list[int]: return sorted(validate(i) for i in items) """ validate(val, t) return val
[docs] def validated_iter(val: typing.Iterable[T], t: Any) -> typing.Iterable[T]: """ Performs the same functionality as :func:`validated`, but the iterable ``var`` is wrapped into an iterator which validates its items prior to them being yielded. """ validate(val, t) if t in _iterator_origins: return val if hasattr(t, "__origin__") and t.__origin__ in _iterator_origins: # assert hasattr(t, "__args__"), _missing_args_msg(t) # assert ( # isinstance(t.__args__, tuple) and len(t.__args__) == 1 # ), _wrong_args_num_msg(t, 1) item_t = t.__args__[0] return (validated(item, item_t) for item in val) raise ValueError( "Argument 't' must be Iterable, Iterator, Iterable[T], or Iterator[T]." )