File: _subclass_of.py

package info (click to toggle)
python-typish 1.9.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 324 kB
  • sloc: python: 1,632; makefile: 2
file content (158 lines) | stat: -rw-r--r-- 5,709 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import typing

from typish._types import Unknown
from typish.functions._get_alias import get_alias


def subclass_of(cls: object, *args: object) -> bool:
    """
    Return whether ``cls`` is a subclass of all types in ``args`` while also
    considering generics.

    If you want the subclass check to be customized for your type, then make
    sure it has a __subclasscheck__ defined (not in a base class).
    :param cls: the subject.
    :param args: the super types.
    :return: True if ``cls`` is a subclass of all types in ``args`` while also
    considering generics.
    """
    return all(_subclass_of(cls, clsinfo) for clsinfo in args)


def _subclass_of(cls: type, clsinfo: object) -> bool:
    # Check whether cls is a subtype of clsinfo.
    from typish.classes._literal import LiteralAlias

    # Translate to typing type if possible.
    clsinfo = get_alias(clsinfo) or clsinfo

    if _is_true_case(cls, clsinfo):
        result = True
    elif issubclass(clsinfo, LiteralAlias):
        return _check_literal(cls, subclass_of, clsinfo)
    elif is_issubclass_case(cls, clsinfo):
        result = issubclass(cls, clsinfo)
    else:
        result = _forward_subclass_check(cls, clsinfo)

    return result


def _forward_subclass_check(cls: type, clsinfo: type) -> bool:
    # Forward the subclass check for cls and clsinfo to delegates that know how
    # to check that particular cls/clsinfo type.

    from typish.functions._get_origin import get_origin
    from typish.functions._get_args import get_args

    clsinfo_origin = get_origin(clsinfo)
    clsinfo_args = get_args(clsinfo)
    cls_origin = get_origin(cls)

    if cls_origin is typing.Union:
        # cls is a Union; all options of that Union must subclass clsinfo.
        cls_args = get_args(cls)
        result = all([subclass_of(elem, clsinfo) for elem in cls_args])
    elif clsinfo_args:
        result = _subclass_of_generic(cls, clsinfo_origin, clsinfo_args)
    else:
        try:
            result = issubclass(cls_origin, clsinfo_origin)
        except TypeError:
            result = False
    return result


def _subclass_of_generic(
        cls: type,
        info_generic_type: type,
        info_args: typing.Tuple[type, ...]) -> bool:
    # Check if cls is a subtype of info_generic_type, knowing that the latter
    # is a generic type.

    from typish.functions._get_origin import get_origin
    from typish.functions._get_args import get_args

    result = False
    cls_origin = get_origin(cls)
    cls_args = get_args(cls)
    if info_generic_type is tuple:
        # Special case.
        result = (subclass_of(cls_origin, tuple)
                  and _subclass_of_tuple(cls_args, info_args))
    elif info_generic_type is typing.Union:
        # Another special case.
        result = any(subclass_of(cls, cls_) for cls_ in info_args)
    elif cls_origin is tuple and info_generic_type is typing.Iterable:
        # Another special case.
        args = _tuple_args(cls_args)

        # Match the number of arguments of info to that of cls.
        matched_info_args = info_args * len(args)
        result = _subclass_of_tuple(args, matched_info_args)
    elif (subclass_of(cls_origin, info_generic_type) and cls_args
          and len(cls_args) == len(info_args)):
        result = all(subclass_of(*tup) for tup in zip(cls_args, info_args))
    # Note that issubtype(list, List[...]) is always False.
    # Note that the number of arguments must be equal.
    return result


def _subclass_of_tuple(
        cls_args: typing.Tuple[type, ...],
        info_args: typing.Tuple[type, ...]) -> bool:
    from typish.functions._get_origin import get_origin
    from typish.functions._common_ancestor import common_ancestor_of_types

    result = False
    if len(info_args) == 2 and info_args[1] is ...:
        type_ = get_origin(info_args[0])
        if type_ is typing.Union:
            # A heterogeneous tuple: check each element if it subclasses the
            # union.
            result = all([subclass_of(elem, info_args[0]) for elem in cls_args])
        else:
            result = subclass_of(common_ancestor_of_types(*cls_args), info_args[0])
    elif len(cls_args) == len(info_args):
        result = all(subclass_of(c1, c2)
                     for c1, c2 in zip(cls_args, info_args))
    return result


def _check_literal(obj: object, func: typing.Callable, *args: type) -> bool:
    # Instance or subclass check for Literal.
    literal = args[0]
    leftovers = args[1:]
    literal_args = getattr(literal, '__args__', None)
    result = False
    if literal_args:
        literal_arg = literal_args[0]
        result = (obj == literal_arg
                  and (not leftovers or func(obj, *leftovers)))
    return result


def _is_true_case(cls: type, clsinfo: type) -> bool:
    # Return whether subclass_of(cls, clsinfo) holds a case that must always be
    # True, without the need of further checking.
    return cls == clsinfo or cls is Unknown or clsinfo in (typing.Any, object)


def is_issubclass_case(cls: type, clsinfo: type) -> bool:
    # Return whether subclass_of(cls, clsinfo) holds a case that can be handled
    # by the builtin issubclass.
    from typish.functions._is_from_typing import is_from_typing

    return (not is_from_typing(clsinfo)
            and isinstance(cls, type)
            and clsinfo is not type
            and '__subclasscheck__' in dir(clsinfo))


def _tuple_args(
        cls_args: typing.Iterable[typing.Any]) -> typing.Iterable[type]:
    # Get the argument types from a tuple, even if the form is Tuple[int, ...].
    result = cls_args
    if len(cls_args) > 1 and cls_args[1] is ...:
        result = [cls_args[0]]
    return result