def is_subtype()

in chz/tiepin.py [0:0]


def is_subtype(left: TypeForm, right: TypeForm) -> bool:
    left_origin = getattr(left, "__origin__", left)
    left_args = getattr(left, "__args__", ())
    right_origin = getattr(right, "__origin__", right)
    right_args = getattr(right, "__args__", ())

    if left_origin is typing.Any or left_origin is typing_extensions.Any:
        return True
    if right_origin is typing.Any or right_origin is typing_extensions.Any:
        return True
    if left_origin is None:
        if right_origin is None or right_origin is type(None):
            return True

    if is_union_type(right_origin):
        if is_union_type(left_origin):
            possible_left_types = left_args
        else:
            possible_left_types = [left]
        return all(
            any(is_subtype(possible_left, right_arg) for right_arg in right_args)
            for possible_left in possible_left_types
        )

    if right_origin is typing.Literal or right_origin is typing_extensions.Literal:
        if left_origin is typing.Literal or left_origin is typing_extensions.Literal:
            return all(left_arg in right_args for left_arg in left_args)
        return False

    if left_origin is typing.Literal or left_origin is typing_extensions.Literal:
        return all(is_subtype_instance(left_arg, right) for left_arg in left_args)

    if isinstance(left_origin, typing.TypeVar):
        if left_origin == right_origin:
            return True
        bound = left_origin.__bound__
        if bound is None:
            bound = object
        if is_subtype(bound, right_origin):
            return True
        if left_origin.__constraints__:
            return any(
                is_subtype(left_arg, right_origin) for left_arg in left_origin.__constraints__
            )
        return False

    if typing_extensions.is_protocol(left) and typing_extensions.is_protocol(right):
        left_attrs = typing_extensions.get_protocol_members(left)
        right_attrs = typing_extensions.get_protocol_members(right)
        if not right_attrs.issubset(left_attrs):
            return False

        # TODO: this is incorrect
        return True

    if typing_extensions.is_protocol(right):
        if not isinstance(left_origin, type):
            return False

        right_attrs = typing_extensions.get_protocol_members(right)
        if not all(hasattr(left_origin, attr) for attr in right_attrs):
            return False

        # TODO: this is incorrect
        return True

    if isinstance(left, _SignatureOf) and isinstance(right, _SignatureOf):
        empty = inspect.Parameter.empty

        for left_param, right_param in zip(left.pos, right.pos):
            if right_param.kind is right_param.POSITIONAL_OR_KEYWORD:
                if right_param.name != left_param.name:
                    return False
                if left_param.kind is left_param.POSITIONAL_ONLY:
                    return False
            if right_param.default is not empty and left_param.default is empty:
                return False
            left_param_annot = maybe_eval_in_context(left_param.annotation, left.fn)
            right_param_annot = maybe_eval_in_context(right_param.annotation, right.fn)
            if not is_subtype(right_param_annot, left_param_annot):
                return False

        if len(left.pos) < len(right.pos):
            # Okay if left has a *args that accepts all the extra args
            if left.varpos is None:
                return False
            left_varpos_annot = maybe_eval_in_context(left.varpos.annotation, left.fn)
            for i in range(len(left.pos), len(right.pos)):
                right_param = right.pos[i]
                right_param_annot = maybe_eval_in_context(right_param.annotation, right.fn)
                if not is_subtype(right_param_annot, left_varpos_annot):
                    return False

        if len(left.pos) > len(right.pos):
            # Must either have a default or correspond to a required keyword-only arg
            for i in range(len(right.pos), len(left.pos)):
                left_param = left.pos[i]
                if left_param.default is not empty:
                    continue
                if (
                    left_param.name in right.kwonly
                    and left_param.kind is left_param.POSITIONAL_OR_KEYWORD
                ):
                    continue
                return False

        for name in left.kwonly.keys() & right.kwonly.keys():
            right_param = right.kwonly[name]
            left_param = left.kwonly[name]
            if right_param.default is not empty and left_param.default is empty:
                return False
            left_param_annot = maybe_eval_in_context(left_param.annotation, left.fn)
            right_param_annot = maybe_eval_in_context(right_param.annotation, right.fn)
            if not is_subtype(right_param_annot, left_param_annot):
                return False

        for name in left.kwonly.keys() - right.kwonly.keys():
            # Must either have a default or match a varkwarg
            left_param = left.kwonly[name]
            if left_param.default is not empty:
                continue
            if right.varkw is not None:
                left_param_annot = maybe_eval_in_context(left_param.annotation, left.fn)
                right_varkw_annot = maybe_eval_in_context(right.varkw.annotation, right.fn)
                if is_subtype(right_varkw_annot, left_param_annot):
                    continue
            return False

        right_only_kwonly = right.kwonly.keys() - left.kwonly.keys()
        if right_only_kwonly:
            # Must correspond to a positional-or-keyword arg
            left_pos_or_kw = {p.name: p for p in left.pos if p.kind is p.POSITIONAL_OR_KEYWORD}
            for name in right_only_kwonly:
                if name not in left_pos_or_kw:
                    return False
                left_param = left_pos_or_kw[name]
                if right.kwonly[name].default is not empty and left_param.default is empty:
                    return False
                left_param_annot = maybe_eval_in_context(left_param.annotation, left.fn)
                right_param_annot = maybe_eval_in_context(right.kwonly[name].annotation, right.fn)
                if not is_subtype(right_param_annot, left_param_annot):
                    return False

        if right.varkw is not None:
            if left.varkw is None:
                return False
            right_varkw_annot = maybe_eval_in_context(right.varkw.annotation, right.fn)
            left_varkw_annot = maybe_eval_in_context(left.varkw.annotation, left.fn)
            if not is_subtype(right_varkw_annot, left_varkw_annot):
                return False

        if right.ret is not empty and left.ret is not empty:
            # TODO: handle Cls.__init__ like below
            if not is_subtype(left.ret, right.ret):
                return False

        return True

    if left_origin is collections.abc.Callable and right_origin is collections.abc.Callable:
        *left_params, left_ret = left_args
        *right_params, right_ret = right_args
        if len(left_params) != len(right_params):
            return False
        if not is_subtype(left_ret, right_ret):
            return False
        return all(
            is_subtype(right_param, left_param)
            for left_param, right_param in zip(left_params, right_params)
        )

    # TODO: handle other special forms

    try:
        if not issubclass(left_origin, right_origin):
            return False
    except TypeError:
        return False

    # see comments in is_subtype_instance
    # TODO: add invariance
    # TODO: think about some of this logic more carefully
    if hasattr(left_origin, "__class_getitem__") and hasattr(right_origin, "__class_getitem__"):
        if (
            issubclass(right_origin, collections.abc.Mapping)
            and typing.Generic not in left_origin.__mro__
            and typing.Generic not in right_origin.__mro__
        ):
            if left_args:
                left_key, left_value = left_args
            else:
                left_key, left_value = typing.Any, typing.Any
            if right_args:
                right_key, right_value = right_args
            else:
                right_key, right_value = typing.Any, typing.Any
            return is_subtype(left_key, right_key) and is_subtype(left_value, right_value)

        if left_origin is tuple and right_origin is tuple:
            if not left_args:
                left_args = (typing.Any, ...)
            if not right_args:
                right_args = (typing.Any, ...)
            if len(right_args) == 2 and right_args[1] is ...:
                return all(is_subtype(left_arg, right_args[0]) for left_arg in left_args)
            if len(left_args) == 2 and left_args[1] is ...:
                return False
            return len(left_args) == len(right_args) and all(
                is_subtype(left_arg, right_arg)
                for left_arg, right_arg in zip(left_args, right_args)
            )

        if (
            issubclass(right_origin, collections.abc.Iterable)
            and typing.Generic not in left_origin.__mro__
            and typing.Generic not in right_origin.__mro__
        ):
            if left_args:
                (left_item,) = left_args
            else:
                left_item = typing.Any
            if right_args:
                (right_item,) = right_args
            else:
                right_item = typing.Any
            return is_subtype(left_item, right_item)

    return True