Test Failed
Push — master ( d00c81...2f33ee )
by Ramon
07:41 queued 06:02
created

typish._functions._get_mro()   A

Complexity

Conditions 3

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
1
"""
2
PRIVATE MODULE: do not import (from) it directly.
3
4
This module contains the implementation of all functions of typish.
5
"""
6
import inspect
7
import sys
8
import types
9
import typing
10
from collections import deque, defaultdict
11
from collections.abc import Set
12
from functools import lru_cache
13
from inspect import getmro
14
from typish._types import T, KT, VT, NoneType, Unknown, Empty
15
16
17
def subclass_of(cls: type, *args: type) -> bool:
18
    """
19
    Return whether ``cls`` is a subclass of all types in ``args`` while also
20
    considering generics.
21
    :param cls: the subject.
22
    :param args: the super types.
23
    :return: True if ``cls`` is a subclass of all types in ``args`` while also
24
    considering generics.
25
    """
26
    if len(args) > 1:
27
        result = subclass_of(cls, args[0]) and subclass_of(cls, *args[1:])
28
    else:
29
        if args[0] == cls:
30
            return True
31
        result = _subclass_of(cls, args[0])
32
    return result
33
34
35
def instance_of(obj: object, *args: type) -> bool:
36
    """
37
    Check whether ``obj`` is an instance of all types in ``args``, while also
38
    considering generics.
39
    :param obj: the object in subject.
40
    :param args: the type(s) of which ``obj`` is an instance or not.
41
    :return: ``True`` if ``obj`` is an instance of all types in ``args``.
42
    """
43
    return subclass_of(get_type(obj), *args)
44
45
46
def get_origin(t: type) -> type:
47
    """
48
    Return the origin of the given (generic) type. For example, for
49
    ``t=List[str]``, the result would be ``list``.
50
    :param t: the type of which the origin is to be found.
51
    :return: the origin of ``t`` or ``t`` if it is not generic.
52
    """
53
    simple_name = _get_simple_name(t)
54
    result = _type_per_alias.get(simple_name, None)
55
    if not result:
56
        result = getattr(typing, simple_name, t)
57
    return result
58
59
60
def get_args(t: type) -> typing.Tuple[type, ...]:
61
    """
62
    Get the arguments from a collection type (e.g. ``typing.List[int]``) as a
63
    ``tuple``.
64
    :param t: the collection type.
65
    :return: a ``tuple`` containing types.
66
    """
67
    args_ = getattr(t, '__args__', tuple()) or tuple()
68
    args = tuple([attr for attr in args_
69
                  if type(attr) != typing.TypeVar])
70
    return args
71
72
73
@lru_cache()
74
def get_alias(cls: T) -> typing.Optional[T]:
75
    """
76
    Return the alias from the ``typing`` module for ``cls``. For example, for
77
    ``cls=list``, the result would be ``typing.List``. If no alias exists for
78
    ``cls``, then ``None`` is returned.
79
    :param cls: the type for which the ``typing`` equivalent is to be found.
80
    :return: the alias from ``typing``.
81
    """
82
    return _alias_per_type.get(cls.__name__, None)
83
84
85
def get_type(inst: T) -> typing.Type[T]:
86
    """
87
    Return a type, complete with generics for the given ``inst``.
88
    :param inst: the instance for which a type is to be returned.
89
    :return: the type of ``inst``.
90
    """
91
    result = type(inst)
92
    super_types = [
93
        (dict, _get_type_dict),
94
        (tuple, _get_type_tuple),
95
        (str, lambda inst_: result),
96
        (typing.Iterable, _get_type_iterable),
97
        (types.FunctionType, _get_type_callable),
98
        (types.MethodType, _get_type_callable),
99
        (type, lambda inst_: typing.Type[inst]),
100
    ]
101
102
    for super_type, func in super_types:
103
        if isinstance(inst, super_type):
104
            result = func(inst)
105
            break
106
    return result
107
108
109
def common_ancestor(*args: object) -> type:
110
    """
111
    Get the closest common ancestor of the given objects.
112
    :param args: any objects.
113
    :return: the ``type`` of the closest common ancestor of the given ``args``.
114
    """
115
    return _common_ancestor(args, False)
116
117
118
def common_ancestor_of_types(*args: type) -> type:
119
    """
120
    Get the closest common ancestor of the given classes.
121
    :param args: any classes.
122
    :return: the ``type`` of the closest common ancestor of the given ``args``.
123
    """
124
    return _common_ancestor(args, True)
125
126
127
def get_args_and_return_type(hint: typing.Type[typing.Callable]) \
128
        -> typing.Tuple[typing.Optional[typing.Tuple[type]], typing.Optional[type]]:
129
    """
130
    Get the argument types and the return type of a callable type hint
131
    (e.g. ``Callable[[int], str]).
132
133
    Example:
134
    ```
135
    arg_types, return_type = get_args_and_return_type(Callable[[int], str])
136
    # args_types is (int, )
137
    # return_type is str
138
    ```
139
140
    Example for when ``hint`` has no generics:
141
    ```
142
    arg_types, return_type = get_args_and_return_type(Callable)
143
    # args_types is None
144
    # return_type is None
145
    ```
146
    :param hint: the callable type hint.
147
    :return: a tuple of the argument types (as a tuple) and the return type.
148
    """
149
    if hint in (callable, typing.Callable):
150
        arg_types = None
151
        return_type = None
152
    elif hasattr(hint, '__result__'):
153
        arg_types = hint.__args__
154
        return_type = hint.__result__
155
    else:
156
        arg_types = hint.__args__[0:-1]
157
        return_type = hint.__args__[-1]
158
    return arg_types, return_type
159
160
161
def get_type_hints_of_callable(
162
        func: typing.Callable) -> typing.Dict[str, type]:
163
    """
164
    Return the type hints of the parameters of the given callable.
165
    :param func: the callable of which the type hints are to be returned.
166
    :return: a dict with parameter names and their types.
167
    """
168
    # Python3.5: get_type_hints raises on classes without explicit constructor
169
    try:
170
        result = typing.get_type_hints(func)
171
    except AttributeError:
172
        result = {}
173
    return result
174
175
176
def _subclass_of_generic(
177
        cls: type,
178
        info_generic_type: type,
179
        info_args: typing.Tuple[type, ...]) -> bool:
180
    # Check if cls is a subtype of info_generic_type, knowing that the latter
181
    # is a generic type.
182
    result = False
183
    cls_generic_type, cls_args = _split_generic(cls)
184
    if info_generic_type is tuple:
185
        # Special case.
186
        result = _subclass_of_tuple(cls_args, info_args)
187
    elif info_generic_type is typing.Union:
188
        # Another special case.
189
        result = _subclass_of_union(cls, info_args)
190
    elif (cls_generic_type == info_generic_type and cls_args
191
            and len(cls_args) == len(info_args)):
192
        for tup in zip(cls_args, info_args):
193
            if not subclass_of(*tup):
194
                result = False
195
                break
196
        else:
197
            result = True
198
    # Note that issubtype(list, List[...]) is always False.
199
    # Note that the number of arguments must be equal.
200
    return result
201
202
203
def _subclass_of_tuple(
204
        cls_args: typing.Tuple[type, ...],
205
        info_args: typing.Tuple[type, ...]) -> bool:
206
    result = False
207
    if len(info_args) == 2 and info_args[1] is ...:
208
        result = subclass_of(common_ancestor_of_types(*cls_args), info_args[0])
209
    elif len(cls_args) == len(info_args):
210
        for c1, c2 in zip(cls_args, info_args):
211
            if not subclass_of(c1, c2):
212
                break
213
        else:
214
            result = True
215
    return result
216
217
218
def _split_generic(t: type) -> \
219
        typing.Tuple[type, typing.Optional[typing.Tuple[type, ...]]]:
220
    # Split the given generic type into the type and its args.
221
    return get_origin(t), get_args(t)
222
223
224
def _get_type_iterable(inst: typing.Iterable):
225
    typing_type = get_alias(type(inst))
226
    common_cls = Unknown
227
    if inst:
228
        common_cls = common_ancestor(*inst)
229
        if typing_type:
230
            if issubclass(common_cls, typing.Iterable) and typing_type is not str:
231
                # Get to the bottom of it; obtain types recursively.
232
                common_cls = get_type(common_cls(_flatten(inst)))
233
    result = typing_type[common_cls]
234
    return result
235
236
237
def _get_type_tuple(inst: tuple) -> typing.Dict[KT, VT]:
238
    args = [get_type(elem) for elem in inst]
239
    return typing.Tuple[tuple(args)]
240
241
242
def _get_type_callable(
243
        inst: typing.Callable) -> typing.Type[typing.Dict[KT, VT]]:
244
    if 'lambda' in str(inst):
245
        result = _get_type_lambda(inst)
246
    else:
247
        result = typing.Callable
248
        sig = inspect.signature(inst)
249
        args = [_map_empty(param.annotation)
250
                for param in sig.parameters.values()]
251
        return_type = NoneType
252
        if sig.return_annotation != Empty:
253
            return_type = sig.return_annotation
254
        if args or return_type != NoneType:
255
            if inspect.iscoroutinefunction(inst):
256
                return_type = typing.Awaitable[return_type]
257
            result = typing.Callable[args, return_type]
258
    return result
259
260
261
def _map_empty(annotation: type) -> type:
262
    result = annotation
263
    if annotation == Empty:
264
        result = typing.Any
265
    return result
266
267
268
def _get_type_lambda(
269
        inst: typing.Callable) -> typing.Type[typing.Dict[KT, VT]]:
270
    args = [Unknown for _ in inspect.signature(inst).parameters]
271
    return_type = Unknown
272
    return typing.Callable[args, return_type]
273
274
275
def _get_type_dict(inst: typing.Dict[KT, VT]) -> typing.Type[typing.Dict[KT, VT]]:
276
    t_list_k = _get_type_iterable(list(inst.keys()))
277
    t_list_v = _get_type_iterable(list(inst.values()))
278
    _, t_k_tuple = _split_generic(t_list_k)
279
    _, t_v_tuple = _split_generic(t_list_v)
280
    return typing.Dict[t_k_tuple[0], t_v_tuple[0]]
281
282
283
def _flatten(l: typing.Iterable[typing.Iterable[typing.Any]]) -> typing.List[typing.Any]:
284
    result = []
285
    for x in l:
286
        result += [*x]
287
    return result
288
289
290
def _common_ancestor(args: typing.Sequence[object], types: bool) -> type:
291
    if len(args) < 1:
292
        raise TypeError('common_ancestor() requires at least 1 argument')
293
    tmap = (lambda x: x) if types else get_type
294
    mros = [_get_mro(tmap(elem)) for elem in args]
295
    for cls in mros[0]:
296
        for mro in mros:
297
            if cls not in mro:
298
                break
299
        else:
300
            # cls is in every mro; a common ancestor is found!
301
            return cls
302
303
304
def _subclass_of(cls: type, clsinfo: type) -> bool:
305
    clsinfo_origin, info_args = _split_generic(clsinfo)
306
    cls_origin = get_origin(cls)
307
    if cls is Unknown or clsinfo in (typing.Any, object):
308
        result = True
309
    elif cls is typing.Any:
310
        result = False
311
    elif cls_origin is typing.Union:
312
        _, cls_args = _split_generic(cls)
313
        result = _union_subclass_of(cls_args, clsinfo)
314
    elif info_args:
315
        result = _subclass_of_generic(cls, clsinfo_origin, info_args)
316
    else:
317
        try:
318
            result = issubclass(cls_origin, clsinfo_origin)
319
        except TypeError:
320
            result = False
321
    return result
322
323
324
def _union_subclass_of(
325
        cls_args: typing.Tuple[type, ...],
326
        clsinfo: type) -> bool:
327
    # Handle subclass_of(union, *)
328
    if sys.version_info[1] in (5, 6):
329
        raise TypeError('typish does not support Unions for Python versions '
330
                        'below 3.7')
331
    result = True
332
    for cls in cls_args:
333
        if subclass_of(cls, clsinfo):
334
            break
335
    else:
336
        result = False
337
    return result
338
339
340
def _subclass_of_union(
341
        cls: type,
342
        info_args: typing.Tuple[type, ...]) -> bool:
343
    # Handle subclass_of(*, union)
344
    result = True
345
    for cls_ in info_args:
346
        if subclass_of(cls, cls_):
347
            break
348
    else:
349
        result = False
350
    return result
351
352
353
@lru_cache()
354
def _get_simple_name(cls: type) -> str:
355
    if cls is None:
356
        cls = type(cls)
357
    cls_name = getattr(cls, '__name__', None)
358
    if not cls_name:
359
        cls_name = getattr(cls, '_name', None)
360
    if not cls_name:
361
        cls_name = repr(cls)
362
        cls_name = cls_name.split('[')[0]  # Remove generic types.
363
        cls_name = cls_name.split('.')[-1]  # Remove any . caused by repr.
364
        cls_name = cls_name.split(r"'>")[0]  # Remove any '>.
365
    return cls_name
366
367
368
def _get_mro(cls: type) -> typing.Tuple[type, ...]:
369
    # Wrapper around ``getmro`` to allow types from ``Typing``.
370
    if cls is ...:
371
        return Ellipsis, object
372
    origin, args = _split_generic(cls)
373
    if origin != cls:
374
        return _get_mro(origin)
375
    return getmro(cls)
376
377
378
_alias_per_type = {
379
    'list': typing.List,
380
    'tuple': typing.Tuple,
381
    'dict': typing.Dict,
382
    'set': typing.Set,
383
    'frozenset': typing.FrozenSet,
384
    'deque': typing.Deque,
385
    'defaultdict': typing.DefaultDict,
386
    'type': typing.Type,
387
    'Set': typing.AbstractSet,
388
}
389
390
_type_per_alias = {
391
    'List': list,
392
    'Tuple': tuple,
393
    'Dict': dict,
394
    'Set': set,
395
    'FrozenSet': frozenset,
396
    'Deque': deque,
397
    'DefaultDict': defaultdict,
398
    'Type': type,
399
    'AbstractSet': Set,
400
}
401