codekingpro/portable-devtools
114k
1"""2Backport Python3.8+ typing utils & issubtype & more3 4567 8## Install9 10``` bash11 pip install typing_utils12```13"""14 15import collections.abc16import io17import itertools18import types19import typing20 21if hasattr(typing, "ForwardRef"): # python3.822 ForwardRef = getattr(typing, "ForwardRef")23elif hasattr(typing, "_ForwardRef"): # python3.624 ForwardRef = getattr(typing, "_ForwardRef")25else:26 raise NotImplementedError()27 28if hasattr(typing, "Literal"):29 Literal = getattr(typing, "Literal")30else:31 Literal = None32 33if hasattr(typing, "_TypedDictMeta"):34 _TypedDictMeta = getattr(typing, "_TypedDictMeta")35else:36 _TypedDictMeta = None37 38if hasattr(types, "UnionType"):39 UnionType = getattr(types, "UnionType")40else:41 UnionType = None42 43unknown = None44 45BUILTINS_MAPPING = {46 typing.List: list,47 typing.Set: set,48 typing.Dict: dict,49 typing.Tuple: tuple,50 typing.ByteString: bytes, # https://docs.python.org/3/library/typing.html#typing.ByteString51 typing.Callable: collections.abc.Callable,52 typing.Sequence: collections.abc.Sequence,53 type(None): None,54}55 56STATIC_SUBTYPE_MAPPING: typing.Dict[type, typing.Type] = {57 io.TextIOWrapper: typing.TextIO,58 io.TextIOBase: typing.TextIO,59 io.StringIO: typing.TextIO,60 io.BufferedReader: typing.BinaryIO,61 io.BufferedWriter: typing.BinaryIO,62 io.BytesIO: typing.BinaryIO,63}64 65if UnionType:66 67 def is_union(element: object) -> bool:68 return element is typing.Union or element is UnionType69 70else:71 72 def is_union(element: object) -> bool:73 return element is typing.Union74 75 76def optional_all(elements) -> typing.Optional[bool]:77 if all(elements):78 return True79 if all(e is False for e in elements):80 return False81 return unknown82 83 84def optional_any(elements) -> typing.Optional[bool]:85 if any(elements):86 return True87 if any(e is None for e in elements):88 return unknown89 return False90 91 92def _hashable(value):93 """Determine whether `value` can be hashed."""94 try:95 hash(value)96 except TypeError:97 return False98 return True99 100 101get_type_hints = typing.get_type_hints102 103GenericClass = type(typing.List)104UnionClass = type(typing.Union)105 106Type = typing.Union[None, type, "typing.TypeVar"]107OriginType = typing.Union[None, type]108TypeArgs = typing.Union[type, typing.AbstractSet[type], typing.Sequence[type]]109 110 111def _normalize_aliases(type_: Type) -> Type:112 if isinstance(type_, typing.TypeVar):113 return type_114 115 assert _hashable(type_), "_normalize_aliases should only be called on element types"116 117 if type_ in BUILTINS_MAPPING:118 return BUILTINS_MAPPING[type_] # type: ignore119 return type_120 121 122def get_origin(type_):123 """Get the unsubscripted version of a type.124 This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.125 Return None for unsupported types.126 127 Examples:128 129 ```python130 from typing_utils import get_origin131 132 get_origin(Literal[42]) is Literal133 get_origin(int) is None134 get_origin(ClassVar[int]) is ClassVar135 get_origin(Generic) is Generic136 get_origin(Generic[T]) is Generic137 get_origin(Union[T, int]) is Union138 get_origin(List[Tuple[T, T]][int]) == list139 ```140 """141 if hasattr(typing, "get_origin"): # python 3.8+142 _getter = getattr(typing, "get_origin")143 ori = _getter(type_)144 elif hasattr(typing.List, "_special"): # python 3.7145 if isinstance(type_, GenericClass) and not type_._special:146 ori = type_.__origin__147 elif hasattr(type_, "_special") and type_._special:148 ori = type_149 elif type_ is typing.Generic:150 ori = typing.Generic151 else:152 ori = None153 else: # python 3.6154 if isinstance(type_, GenericClass):155 ori = type_.__origin__156 if ori is None:157 ori = type_158 elif isinstance(type_, UnionClass):159 ori = type_.__origin__160 elif type_ is typing.Generic:161 ori = typing.Generic162 else:163 ori = None164 if ori is None and _TypedDictMeta and isinstance(type_, _TypedDictMeta):165 ori = dict166 return _normalize_aliases(ori)167 168 169def get_args(type_) -> typing.Tuple:170 """Get type arguments with all substitutions performed.171 For unions, basic simplifications used by Union constructor are performed.172 173 Examples:174 175 ```python176 from typing_utils import get_args177 178 get_args(Dict[str, int]) == (str, int)179 get_args(int) == ()180 get_args(Union[int, Union[T, int], str][int]) == (int, str)181 get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])182 get_args(Callable[[], T][int]) == ([], int)183 ```184 """185 if hasattr(typing, "get_args"): # python 3.8+186 _getter = getattr(typing, "get_args")187 res = _getter(type_)188 elif hasattr(typing.List, "_special"): # python 3.7189 if (190 isinstance(type_, GenericClass) and not type_._special # type: ignore191 ): # backport for python 3.8192 res = type_.__args__ # type: ignore193 if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:194 res = (list(res[:-1]), res[-1])195 else:196 res = ()197 else: # python 3.6198 if isinstance(type_, (GenericClass, UnionClass)): # backport for python 3.8199 res = type_.__args__ # type: ignore200 if get_origin(type_) is collections.abc.Callable and res[0] is not Ellipsis:201 res = (list(res[:-1]), res[-1])202 else:203 res = ()204 if _TypedDictMeta and isinstance(type_, _TypedDictMeta):205 return str, typing.Any206 return () if res is None else res207 208 209def eval_forward_ref(ref, forward_refs=None):210 """211 eval forward_refs in all cPython versions212 """213 localns = forward_refs or {}214 215 if hasattr(typing, "_eval_type"): # python3.8 & python 3.9216 _eval_type = getattr(typing, "_eval_type")217 return _eval_type(ref, globals(), localns)218 219 if hasattr(ref, "_eval_type"): # python3.6220 _eval_type = getattr(ref, "_eval_type")221 return _eval_type(globals(), localns)222 223 raise NotImplementedError()224 225 226class NormalizedType(typing.NamedTuple):227 """228 Normalized type, made it possible to compare, hash between types.229 """230 231 origin: Type232 args: typing.Union[tuple, frozenset] = tuple()233 234 def __eq__(self, other):235 if isinstance(other, NormalizedType):236 if self.origin != other.origin:237 return False238 if isinstance(self.args, frozenset) and isinstance(other.args, frozenset):239 return self.args <= other.args and other.args <= self.args240 return self.origin == other.origin and self.args == other.args241 if not self.args:242 return self.origin == other243 return False244 245 def __hash__(self) -> int:246 if not self.args:247 return hash(self.origin)248 return hash((self.origin, self.args))249 250 def __repr__(self):251 if not self.args:252 return f"{self.origin}"253 return f"{self.origin}[{self.args}])"254 255 256def _normalize_args(tps: TypeArgs):257 if isinstance(tps, str):258 return tps259 if isinstance(tps, collections.abc.Sequence):260 return tuple(_normalize_args(type_) for type_ in tps)261 if isinstance(tps, collections.abc.Set):262 return frozenset(_normalize_args(type_) for type_ in tps)263 return normalize(tps)264 265 266def normalize(type_: Type) -> NormalizedType:267 """268 convert types to NormalizedType instances.269 """270 args = get_args(type_)271 origin = get_origin(type_)272 if not origin:273 return NormalizedType(_normalize_aliases(type_))274 origin = _normalize_aliases(origin)275 276 if is_union(origin): # sort args when the origin is Union277 args = _normalize_args(frozenset(args))278 else:279 args = _normalize_args(args)280 return NormalizedType(origin, args)281 282 283def _is_origin_subtype(left: OriginType, right: OriginType) -> bool:284 if left is right:285 return True286 287 if (288 left is not None289 and left in STATIC_SUBTYPE_MAPPING290 and right == STATIC_SUBTYPE_MAPPING[left]291 ):292 return True293 294 if hasattr(left, "mro"):295 for parent in left.mro(): # type: ignore296 if parent == right:297 return True298 299 if isinstance(left, type) and isinstance(right, type):300 return issubclass(left, right)301 302 return left == right303 304 305NormalizedTypeArgs = typing.Union[306 typing.Tuple[typing.Any, ...],307 typing.FrozenSet[NormalizedType],308 NormalizedType,309]310 311 312def _is_origin_subtype_args(313 left: "NormalizedTypeArgs",314 right: "NormalizedTypeArgs",315 forward_refs: typing.Optional[typing.Mapping[str, type]],316) -> typing.Optional[bool]:317 if isinstance(left, frozenset):318 if not isinstance(right, frozenset):319 return False320 321 excluded = left - right322 if not excluded:323 # Union[str, int] <> Union[int, str]324 return True325 326 # Union[list, int] <> Union[typing.Sequence, int]327 return all(328 any(_is_normal_subtype(e, r, forward_refs) for r in right) for e in excluded329 )330 331 if isinstance(left, collections.abc.Sequence) and not isinstance(332 left, NormalizedType333 ):334 if not isinstance(right, collections.abc.Sequence) or isinstance(335 right, NormalizedType336 ):337 return False338 339 if (340 left341 and left[-1].origin is not Ellipsis342 and right343 and right[-1].origin is Ellipsis344 ):345 # Tuple[type, type] <> Tuple[type, ...]346 return all(_is_origin_subtype_args(l, right[0], forward_refs) for l in left)347 348 if len(left) != len(right):349 return False350 351 return all(352 l is not None353 and r is not None354 and _is_origin_subtype_args(l, r, forward_refs)355 for l, r in itertools.zip_longest(left, right)356 )357 358 assert isinstance(left, NormalizedType)359 assert isinstance(right, NormalizedType)360 361 return _is_normal_subtype(left, right, forward_refs)362 363 364def _is_normal_subtype(365 left: NormalizedType,366 right: NormalizedType,367 forward_refs: typing.Optional[typing.Mapping[str, type]],368) -> typing.Optional[bool]:369 if isinstance(left.origin, ForwardRef):370 left = normalize(eval_forward_ref(left.origin, forward_refs=forward_refs))371 372 if isinstance(right.origin, ForwardRef):373 right = normalize(eval_forward_ref(right.origin, forward_refs=forward_refs))374 375 # Any376 if right.origin is typing.Any:377 return True378 379 # Union380 if is_union(right.origin) and is_union(left.origin):381 return _is_origin_subtype_args(left.args, right.args, forward_refs)382 if is_union(right.origin):383 return optional_any(384 _is_normal_subtype(left, a, forward_refs) for a in right.args385 )386 if is_union(left.origin):387 return optional_all(388 _is_normal_subtype(a, right, forward_refs) for a in left.args389 )390 391 # Literal392 if right.origin is Literal:393 if left.origin is not Literal:394 return False395 return set(left.args).issubset(set(right.args))396 397 # TypeVar398 if isinstance(left.origin, typing.TypeVar) and isinstance(399 right.origin, typing.TypeVar400 ):401 if left.origin is right.origin:402 return True403 404 left_bound = getattr(left.origin, "__bound__", None)405 right_bound = getattr(right.origin, "__bound__", None)406 if right_bound is None or left_bound is None:407 return unknown408 return _is_normal_subtype(409 normalize(left_bound), normalize(right_bound), forward_refs410 )411 if isinstance(right.origin, typing.TypeVar):412 return unknown413 if isinstance(left.origin, typing.TypeVar):414 left_bound = getattr(left.origin, "__bound__", None)415 if left_bound is None:416 return unknown417 return _is_normal_subtype(normalize(left_bound), right, forward_refs)418 419 if not left.args and not right.args:420 return _is_origin_subtype(left.origin, right.origin)421 422 if not right.args:423 return _is_origin_subtype(left.origin, right.origin)424 425 if _is_origin_subtype(left.origin, right.origin):426 return _is_origin_subtype_args(left.args, right.args, forward_refs)427 428 return False429 430 431def issubtype(432 left: Type,433 right: Type,434 forward_refs: typing.Optional[dict] = None,435) -> typing.Optional[bool]:436 """Check that the left argument is a subtype of the right.437 For unions, check if the type arguments of the left is a subset of the right.438 Also works for nested types including ForwardRefs.439 440 Examples:441 442 ```python443 from typing_utils import issubtype444 445 issubtype(typing.List, typing.Any) == True446 issubtype(list, list) == True447 issubtype(list, typing.List) == True448 issubtype(list, typing.Sequence) == True449 issubtype(typing.List[int], list) == True450 issubtype(typing.List[typing.List], list) == True451 issubtype(list, typing.List[int]) == False452 issubtype(list, typing.Union[typing.Tuple, typing.Set]) == False453 issubtype(typing.List[typing.List], typing.List[typing.Sequence]) == True454 JSON = typing.Union[455 int, float, bool, str, None, typing.Sequence["JSON"],456 typing.Mapping[str, "JSON"]457 ]458 issubtype(str, JSON, forward_refs={'JSON': JSON}) == True459 issubtype(typing.Dict[str, str], JSON, forward_refs={'JSON': JSON}) == True460 issubtype(typing.Dict[str, bytes], JSON, forward_refs={'JSON': JSON}) == False461 ```462 """463 return _is_normal_subtype(normalize(left), normalize(right), forward_refs)464 465 466__all__ = [467 "issubtype",468 "get_origin",469 "get_args",470 "get_type_hints",471]472 