def is_subtype_instance()

in chz/tiepin.py [0:0]


def is_subtype_instance(inst: typing.Any, typ: TypeForm) -> bool:
    if typ is typing.Any or typ is typing_extensions.Any:
        return True

    if typ is None and inst is None:
        return True

    if isinstance(typ, typing.TypeVar):
        if typ.__constraints__:
            # types must match exactly
            return any(
                type(inst) is getattr(c, "__origin__", c) and is_subtype_instance(inst, c)
                for c in typ.__constraints__
            )
        if typ.__bound__:
            return is_subtype_instance(inst, typ.__bound__)
        return True

    if isinstance(typ, typing.NewType):
        return isinstance(inst, typ.__supertype__)

    origin: typing.Any
    args: typing.Any
    if sys.version_info >= (3, 10) and isinstance(typ, types.UnionType):
        origin = typing.Union
    else:
        origin = getattr(typ, "__origin__", typ)

    args = getattr(typ, "__args__", ())
    del typ

    if origin is typing.Union:
        return any(is_subtype_instance(inst, t) for t in args)
    if origin is typing.Literal or origin is typing_extensions.Literal:
        return inst in args

    if origin is typing.LiteralString:
        return isinstance(inst, str)

    if is_typed_dict(origin):
        if not isinstance(inst, dict):
            return False

        for k, v in typing_extensions.get_type_hints(origin).items():
            if k in inst:
                if not is_subtype_instance(inst[k], v):
                    return False
            elif k in origin.__required_keys__:
                return False
        return True

    # Pydantic implements generics in a special way. Just delegate validation to Pydantic.
    # Note that all pydantic models have __pydantic_generic_metadata__, even non-generic ones.
    if hasattr(origin, "__pydantic_generic_metadata__"):
        from pydantic import ValidationError

        try:
            origin.model_validate(inst)
            return True
        except ValidationError:
            return False

    if typing_extensions.is_protocol(origin):
        if getattr(origin, "_is_runtime_protocol", False):
            return isinstance(inst, origin)
        if origin in type(inst).__mro__:
            return True
        annotations = typing_extensions.get_type_hints(origin)
        for attr in sorted(typing_extensions.get_protocol_members(origin)):
            if not hasattr(inst, attr):
                return False
            if attr in annotations:
                if not is_subtype_instance(getattr(inst, attr), annotations[attr]):
                    return False
            elif callable(getattr(origin, attr)):
                if attr == "__call__" and isinstance(inst, (type, types.FunctionType)):
                    # inst will have a better inspect.signature than inst.__call__
                    inst_attr = inst
                else:
                    inst_attr = getattr(inst, attr)

                if not callable(inst_attr):
                    return False
                try:
                    signature = _SignatureOf(getattr(origin, attr), strip_self=True)
                except ValueError:
                    continue
                if not is_subtype_instance(inst_attr, signature):
                    return False
            else:
                raise AssertionError(f"Unexpected protocol member {attr} for {origin}")
        return True

    if isinstance(origin, _SignatureOf):
        try:
            inst_sig = _SignatureOf(inst)
        except ValueError:
            return True

        return is_subtype(inst_sig, origin)

    # We're done handling special forms, now just need to handle things like generics
    if not isinstance(origin, type):
        # TODO: handle other special forms before exit on this branch
        return False
    if not isinstance(inst, origin):
        # PEP 484 duck type compatibility
        if origin is complex and isinstance(inst, (int, float)):
            return True
        if origin is float and isinstance(inst, int):
            return True
        if origin is bytes and isinstance(inst, (bytearray, memoryview)):
            # TODO: maybe remove bytearray and memoryview ducktyping based on PEP 688
            return True

        if inst in typing_Never:
            return True
        if issubclass(type(inst), typing_extensions.Any) or (
            sys.version_info >= (3, 11) and issubclass(type(inst), typing.Any)
        ):
            return True
        return False

    assert isinstance(inst, origin)
    if not args:
        return True

    # TODO: there's some confusion when checking issubclass against a generic collections.abc
    # base class, since you don't actually know whether the generic args of typ / origin correspond
    # to the generic args of the base class. So if we detect a user defined generic (i.e. based
    # on presence of Generic in the mro), we just fall back and don't assume we know the semantics
    # of what the generic args are.
    if issubclass(origin, collections.abc.Mapping) and typing.Generic not in origin.__mro__:
        key_type, value_type = args
        return all(
            is_subtype_instance(key, key_type) and is_subtype_instance(value, value_type)
            for key, value in inst.items()
        )
    if origin is tuple:
        if len(args) == 2 and args[1] is ...:
            return all(is_subtype_instance(i, args[0]) for i in inst)
        if len(inst) != len(args):
            return False
        return all(is_subtype_instance(i, t) for i, t in zip(inst, args))

    if issubclass(origin, collections.abc.Iterable) and typing.Generic not in origin.__mro__:
        (item_type,) = args
        return all(is_subtype_instance(item, item_type) for item in inst)

    if origin is type:
        (type_type,) = args
        return issubclass(inst, type_type)

    if origin is collections.abc.Callable:
        try:
            inst_sig = inspect.signature(inst)
        except ValueError:
            return True
        *params, ret = args
        if params != [...]:
            try:
                bound = inst_sig.bind(*params)
            except TypeError:
                return False
            for param, callable_param_type in bound.arguments.items():
                param = inst_sig.parameters[param]
                param_annot = maybe_eval_in_context(param.annotation, inst)
                # ooh, contravariance
                if param.kind is param.VAR_POSITIONAL:
                    if any(not is_subtype(cpt, param_annot) for cpt in callable_param_type):
                        return False
                elif not is_subtype(callable_param_type, param_annot):
                    return False
        if inst_sig.return_annotation is not inst_sig.empty:
            ret_annot = maybe_eval_in_context(inst_sig.return_annotation, inst)
            # inspect.signature(Cls) will have Cls.__init__, which is annotated as -> None
            if not (isinstance(inst, type) and ret_annot is None and is_subtype(inst, ret)):
                if ret_annot is None:
                    ret_annot = type(None)
                elif ret_annot in typing_Never:
                    ret_annot = ret
                if ret_annot != ret and not is_subtype(ret_annot, ret):
                    return False
        return True

    # We don't really know how to handle user defined generics
    if hasattr(inst, "__orig_class__"):
        # If we have an __orig_class__ and the origins match, check the args (assuming that they
        # are invariant, although maybe covariant is a better guess?)
        if inst.__orig_class__.__origin__ is origin:
            return inst.__orig_class__.__args__ == args
    # Otherwise, fail open
    return True