| 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 |