Passed
Push — master ( 781642...0cf0fb )
by Ramon
38s queued 11s
created

typish._functions.subclass_of()   A

Complexity

Conditions 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 19
Ratio 100 %

Importance

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