from inspect import Parameter, signature from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, cast from apischema.types import AnyType from apischema.typing import ( get_type_hints, is_annotated, is_literal, is_new_type, is_type, is_union, ) from apischema.utils import get_origin_or_type Converter = Callable[[Any], Any] def converter_types( converter: Converter, source: AnyType = None, target: AnyType = None, namespace: Optional[Dict[str, Any]] = None, ) -> Tuple[AnyType, AnyType]: try: # in pre 3.9, Generic __new__ perturb signature of types if ( isinstance(converter, type) and converter.__new__ is Generic.__new__ is not object.__new__ and converter.__init__ is not object.__init__ # type: ignore ): parameters = list(signature(converter.__init__).parameters.values())[1:] # type: ignore else: parameters = list(signature(converter).parameters.values()) except ValueError: # builtin types if target is None and is_type(converter): target = cast(Type[Any], converter) if source is None: raise TypeError("Converter source is unknown") from None else: if not parameters: raise TypeError("converter must have at least one parameter") first_param, *other_params = parameters for p in other_params: if p.default is Parameter.empty and p.kind not in ( Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD, ): raise TypeError( "converter must have at most one parameter without default" ) if source is not None and target is not None: return source, target types = get_type_hints(converter, None, namespace, include_extras=True) if not types and is_type(converter): types = get_type_hints( converter.__new__, None, namespace, include_extras=True ) or get_type_hints( converter.__init__, None, namespace, include_extras=True # type: ignore ) if source is None: try: source = types.pop(first_param.name) except KeyError: raise TypeError("converter source is unknown") from None if target is None: if is_type(converter): target = cast(Type, converter) else: try: target = types.pop("return") except KeyError: raise TypeError("converter target is unknown") from None return source, target def is_convertible(tp: AnyType) -> bool: origin = get_origin_or_type(tp) return is_new_type(tp) or ( is_type(origin) and not (tp is Any or is_literal(tp) or is_annotated(tp) or is_union(origin)) )