Total Complexity | 96 |
Total Lines | 453 |
Duplicated Lines | 54.08 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like typish._functions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
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: |
|
|
|||
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: |
|
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]: |
|
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), |
||
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]) \ |
|
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( |
|
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( |
|
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): |
|
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( |
|
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: |
|
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: |
|
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( |
|
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() |
|
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, ...]: |
|
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 |