comment here

This commit is contained in:
ton
2023-03-18 20:03:34 +07:00
commit 4553a0a589
14513 changed files with 2685043 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import typing
def common_ancestor(*args: object) -> type:
"""
Get the closest common ancestor of the given objects.
:param args: any objects.
:return: the ``type`` of the closest common ancestor of the given ``args``.
"""
return _common_ancestor(args, False)
def common_ancestor_of_types(*args: type) -> type:
"""
Get the closest common ancestor of the given classes.
:param args: any classes.
:return: the ``type`` of the closest common ancestor of the given ``args``.
"""
return _common_ancestor(args, True)
def _common_ancestor(args: typing.Sequence[object], types: bool) -> type:
from typish.functions._get_type import get_type
from typish.functions._get_mro import get_mro
if len(args) < 1:
raise TypeError('common_ancestor() requires at least 1 argument')
tmap = (lambda x: x) if types else get_type
mros = [get_mro(tmap(elem)) for elem in args]
for cls in mros[0]:
for mro in mros:
if cls not in mro:
break
else:
# cls is in every mro; a common ancestor is found!
return cls

View File

@@ -0,0 +1,40 @@
import typing
from functools import lru_cache
from typish.functions._is_from_typing import is_from_typing
from typish._types import T
@lru_cache()
def get_alias(cls: T) -> typing.Optional[T]:
"""
Return the alias from the ``typing`` module for ``cls``. For example, for
``cls=list``, the result would be ``typing.List``. If ``cls`` is
parameterized (>=3.9), then a parameterized ``typing`` equivalent is
returned. If no alias exists for ``cls``, then ``None`` is returned.
If ``cls`` already is from ``typing`` it is returned as is.
:param cls: the type for which the ``typing`` equivalent is to be found.
:return: the alias from ``typing``.
"""
if is_from_typing(cls):
return cls
alias = _alias_per_type.get(cls.__name__, None)
if alias:
args = getattr(cls, '__args__', tuple())
if args:
alias = alias[args]
return alias
_alias_per_type = {
'list': typing.List,
'tuple': typing.Tuple,
'dict': typing.Dict,
'set': typing.Set,
'frozenset': typing.FrozenSet,
'deque': typing.Deque,
'defaultdict': typing.DefaultDict,
'type': typing.Type,
'Set': typing.AbstractSet,
}

View File

@@ -0,0 +1,14 @@
import typing
def get_args(t: type) -> typing.Tuple[type, ...]:
"""
Get the arguments from a collection type (e.g. ``typing.List[int]``) as a
``tuple``.
:param t: the collection type.
:return: a ``tuple`` containing types.
"""
args_ = getattr(t, '__args__', tuple()) or tuple()
args = tuple([attr for attr in args_
if type(attr) != typing.TypeVar])
return args

View File

@@ -0,0 +1,31 @@
import typing
from inspect import getmro
def get_mro(obj: typing.Any) -> typing.Tuple[type, ...]:
"""
Return tuple of base classes (including that of obj) in method resolution
order. Types from typing are supported as well.
:param obj: object or type.
:return: a tuple of base classes.
"""
from typish.functions._get_origin import get_origin
# Wrapper around ``getmro`` to allow types from ``typing``.
if obj is ...:
return Ellipsis, object
elif obj is typing.Union:
# For Python <3.7, we cannot use mro.
super_cls = getattr(typing, '_GenericAlias',
getattr(typing, 'GenericMeta', None))
return typing.Union, super_cls, object
origin = get_origin(obj)
if origin != obj:
return get_mro(origin)
cls = obj
if not isinstance(obj, type):
cls = type(obj)
return getmro(cls)

View File

@@ -0,0 +1,39 @@
import typing
from collections import deque, defaultdict
from collections.abc import Set
from inspect import isclass
from typish.functions._is_from_typing import is_from_typing
def get_origin(t: type) -> type:
"""
Return the origin of the given (generic) type. For example, for
``t=List[str]``, the result would be ``list``.
:param t: the type of which the origin is to be found.
:return: the origin of ``t`` or ``t`` if it is not generic.
"""
from typish.functions._get_simple_name import get_simple_name
simple_name = get_simple_name(t)
result = _type_per_alias.get(simple_name, None)
if isclass(t) and not is_from_typing(t):
# Get the origin in case of a parameterized generic.
result = getattr(t, '__origin__', t)
elif not result:
result = getattr(typing, simple_name, t)
return result
_type_per_alias = {
'List': list,
'Tuple': tuple,
'Dict': dict,
'Set': set,
'FrozenSet': frozenset,
'Deque': deque,
'DefaultDict': defaultdict,
'Type': type,
'AbstractSet': Set,
'Optional': typing.Union,
}

View File

@@ -0,0 +1,17 @@
from functools import lru_cache
from typish._types import NoneType
@lru_cache()
def get_simple_name(cls: type) -> str:
cls = cls or NoneType
cls_name = getattr(cls, '__name__', None)
if not cls_name:
cls_name = getattr(cls, '_name', None)
if not cls_name:
cls_name = repr(cls)
cls_name = cls_name.split('[')[0] # Remove generic types.
cls_name = cls_name.split('.')[-1] # Remove any . caused by repr.
cls_name = cls_name.split(r"'>")[0] # Remove any '>.
return cls_name

View File

@@ -0,0 +1,140 @@
import inspect
import types
import typing
from typish._state import DEFAULT, State
from typish._types import T, Unknown, KT, NoneType, Empty, VT
from typish.classes._union_type import UnionType
def get_type(
inst: T,
use_union: bool = False,
*,
state: State = DEFAULT) -> typing.Type[T]:
"""
Return a type, complete with generics for the given ``inst``.
:param inst: the instance for which a type is to be returned.
:param use_union: if ``True``, the resulting type can contain a union.
:param state: any state that is used by typish.
:return: the type of ``inst``.
"""
get_type_for_inst = state.get_type_per_cls.get(type(inst))
if get_type_for_inst:
return get_type_for_inst(inst)
if inst is typing.Any:
return typing.Any
if isinstance(inst, UnionType):
return UnionType
result = type(inst)
super_types = [
(dict, _get_type_dict),
(tuple, _get_type_tuple),
(str, lambda inst_, _, __: result),
(typing.Iterable, _get_type_iterable),
(types.FunctionType, _get_type_callable),
(types.MethodType, _get_type_callable),
(type, lambda inst_, _, __: typing.Type[inst]),
]
try:
for super_type, func in super_types:
if isinstance(inst, super_type):
result = func(inst, use_union, state)
break
except Exception:
# If anything went wrong, return the regular type.
# This is to support 3rd party libraries.
return type(inst)
return result
def _get_type_iterable(
inst: typing.Iterable,
use_union: bool,
state: State) -> type:
from typish.functions._get_alias import get_alias
from typish.functions._common_ancestor import common_ancestor
typing_type = get_alias(type(inst))
common_cls = Unknown
if inst:
if use_union:
types = [get_type(elem, state=state) for elem in inst]
common_cls = typing.Union[tuple(types)]
else:
common_cls = common_ancestor(*inst)
if typing_type:
if issubclass(common_cls, typing.Iterable) and typing_type is not str:
# Get to the bottom of it; obtain types recursively.
common_cls = get_type(common_cls(_flatten(inst)), state=state)
result = typing_type[common_cls]
return result
def _get_type_tuple(
inst: tuple,
use_union: bool,
state: State) -> typing.Dict[KT, VT]:
args = [get_type(elem, state) for elem in inst]
return typing.Tuple[tuple(args)]
def _get_type_callable(
inst: typing.Callable,
use_union: bool,
state: State) -> typing.Type[typing.Dict[KT, VT]]:
if 'lambda' in str(inst):
result = _get_type_lambda(inst, use_union, state)
else:
result = typing.Callable
sig = inspect.signature(inst)
args = [_map_empty(param.annotation)
for param in sig.parameters.values()]
return_type = NoneType
if sig.return_annotation != Empty:
return_type = sig.return_annotation
if args or return_type != NoneType:
if inspect.iscoroutinefunction(inst):
return_type = typing.Awaitable[return_type]
result = typing.Callable[args, return_type]
return result
def _get_type_lambda(
inst: typing.Callable,
use_union: bool,
state: State) -> typing.Type[typing.Dict[KT, VT]]:
args = [Unknown for _ in inspect.signature(inst).parameters]
return_type = Unknown
return typing.Callable[args, return_type]
def _get_type_dict(inst: typing.Dict[KT, VT],
use_union: bool,
state: State) -> typing.Type[typing.Dict[KT, VT]]:
from typish.functions._get_args import get_args
t_list_k = _get_type_iterable(list(inst.keys()), use_union, state)
t_list_v = _get_type_iterable(list(inst.values()), use_union, state)
t_k_tuple = get_args(t_list_k)
t_v_tuple = get_args(t_list_v)
return typing.Dict[t_k_tuple[0], t_v_tuple[0]]
def _flatten(l: typing.Iterable[typing.Iterable[typing.Any]]) -> typing.List[typing.Any]:
result = []
for x in l:
result += [*x]
return result
def _map_empty(annotation: type) -> type:
result = annotation
if annotation == Empty:
result = typing.Any
return result

View File

@@ -0,0 +1,50 @@
import typing
def get_type_hints_of_callable(
func: typing.Callable) -> typing.Dict[str, type]:
"""
Return the type hints of the parameters of the given callable.
:param func: the callable of which the type hints are to be returned.
:return: a dict with parameter names and their types.
"""
# Python3.5: get_type_hints raises on classes without explicit constructor
try:
result = typing.get_type_hints(func)
except AttributeError:
result = {}
return result
def get_args_and_return_type(hint: typing.Type[typing.Callable]) \
-> typing.Tuple[typing.Optional[typing.Tuple[type]], typing.Optional[type]]:
"""
Get the argument types and the return type of a callable type hint
(e.g. ``Callable[[int], str]``).
Example:
```
arg_types, return_type = get_args_and_return_type(Callable[[int], str])
# args_types is (int, )
# return_type is str
```
Example for when ``hint`` has no generics:
```
arg_types, return_type = get_args_and_return_type(Callable)
# args_types is None
# return_type is None
```
:param hint: the callable type hint.
:return: a tuple of the argument types (as a tuple) and the return type.
"""
if hint in (callable, typing.Callable):
arg_types = None
return_type = None
elif hasattr(hint, '__result__'):
arg_types = hint.__args__
return_type = hint.__result__
else:
arg_types = hint.__args__[0:-1]
return_type = hint.__args__[-1]
return arg_types, return_type

View File

@@ -0,0 +1,35 @@
from typish._state import DEFAULT, State
def instance_of(obj: object, *args: object, state: State = DEFAULT) -> bool:
"""
Check whether ``obj`` is an instance of all types in ``args``, while also
considering generics.
If you want the instance check to be customized for your type, then make
sure it has a __instancecheck__ defined (not in a base class). You will
also need to register the get_type function by calling
typish.register_get_type with that particular type and a handling callable.
:param obj: the object in subject.
:param args: the type(s) of which ``obj`` is an instance or not.
:param state: any state that is used by typish.
:return: ``True`` if ``obj`` is an instance of all types in ``args``.
"""
return all(_instance_of(obj, clsinfo, state) for clsinfo in args)
def _instance_of(obj: object, clsinfo: object, state: State = DEFAULT) -> bool:
from typish.classes._literal import LiteralAlias, is_literal_type
from typish.functions._subclass_of import subclass_of
from typish.functions._get_type import get_type
from typish.functions._is_from_typing import is_from_typing
if not is_from_typing(clsinfo) and '__instancecheck__' in dir(clsinfo):
return isinstance(obj, clsinfo)
if is_literal_type(clsinfo):
alias = LiteralAlias.from_literal(clsinfo)
return isinstance(obj, alias)
type_ = get_type(obj, use_union=True, state=state)
return subclass_of(type_, clsinfo)

View File

@@ -0,0 +1,10 @@
import typing
def is_from_typing(cls: type) -> bool:
"""
Return True if the given class is from the typing module.
:param cls: a type.
:return: True if cls is from typing.
"""
return cls.__module__ == typing.__name__

View File

@@ -0,0 +1,23 @@
import typing
from typish import get_origin, get_args, NoneType
def is_optional_type(cls: type) -> bool:
"""
Return True if the given class is an optional type. A type is considered to
be optional if it allows ``None`` as value.
Example:
is_optional_type(Optional[str]) # True
is_optional_type(Union[str, int, None]) # True
is_optional_type(str) # False
is_optional_type(Union[str, int]) # False
:param cls: a type.
:return: True if cls is an optional type.
"""
origin = get_origin(cls)
args = get_args(cls)
return origin == typing.Union and NoneType in args

View File

@@ -0,0 +1,24 @@
import typing
from typish.classes._union_type import UnionType
def is_type_annotation(item: typing.Any) -> bool:
"""
Return whether item is a type annotation (a ``type`` or a type from
``typing``, such as ``List``).
:param item: the item in question.
:return: ``True`` is ``item`` is a type annotation.
"""
from typish.functions._instance_of import instance_of
# Use _GenericAlias for Python 3.7+ and use GenericMeta for the rest.
super_cls = getattr(typing, '_GenericAlias',
getattr(typing, 'GenericMeta', None))
return not isinstance(item, typing.TypeVar) and (
item is typing.Any
or instance_of(item, type)
or instance_of(item, super_cls)
or getattr(item, '__module__', None) == 'typing'
or isinstance(item, UnionType))

View File

@@ -0,0 +1,158 @@
import typing
from typish._types import Unknown
from typish.functions._get_alias import get_alias
def subclass_of(cls: object, *args: object) -> bool:
"""
Return whether ``cls`` is a subclass of all types in ``args`` while also
considering generics.
If you want the subclass check to be customized for your type, then make
sure it has a __subclasscheck__ defined (not in a base class).
:param cls: the subject.
:param args: the super types.
:return: True if ``cls`` is a subclass of all types in ``args`` while also
considering generics.
"""
return all(_subclass_of(cls, clsinfo) for clsinfo in args)
def _subclass_of(cls: type, clsinfo: object) -> bool:
# Check whether cls is a subtype of clsinfo.
from typish.classes._literal import LiteralAlias
# Translate to typing type if possible.
clsinfo = get_alias(clsinfo) or clsinfo
if _is_true_case(cls, clsinfo):
result = True
elif issubclass(clsinfo, LiteralAlias):
return _check_literal(cls, subclass_of, clsinfo)
elif is_issubclass_case(cls, clsinfo):
result = issubclass(cls, clsinfo)
else:
result = _forward_subclass_check(cls, clsinfo)
return result
def _forward_subclass_check(cls: type, clsinfo: type) -> bool:
# Forward the subclass check for cls and clsinfo to delegates that know how
# to check that particular cls/clsinfo type.
from typish.functions._get_origin import get_origin
from typish.functions._get_args import get_args
clsinfo_origin = get_origin(clsinfo)
clsinfo_args = get_args(clsinfo)
cls_origin = get_origin(cls)
if cls_origin is typing.Union:
# cls is a Union; all options of that Union must subclass clsinfo.
cls_args = get_args(cls)
result = all([subclass_of(elem, clsinfo) for elem in cls_args])
elif clsinfo_args:
result = _subclass_of_generic(cls, clsinfo_origin, clsinfo_args)
else:
try:
result = issubclass(cls_origin, clsinfo_origin)
except TypeError:
result = False
return result
def _subclass_of_generic(
cls: type,
info_generic_type: type,
info_args: typing.Tuple[type, ...]) -> bool:
# Check if cls is a subtype of info_generic_type, knowing that the latter
# is a generic type.
from typish.functions._get_origin import get_origin
from typish.functions._get_args import get_args
result = False
cls_origin = get_origin(cls)
cls_args = get_args(cls)
if info_generic_type is tuple:
# Special case.
result = (subclass_of(cls_origin, tuple)
and _subclass_of_tuple(cls_args, info_args))
elif info_generic_type is typing.Union:
# Another special case.
result = any(subclass_of(cls, cls_) for cls_ in info_args)
elif cls_origin is tuple and info_generic_type is typing.Iterable:
# Another special case.
args = _tuple_args(cls_args)
# Match the number of arguments of info to that of cls.
matched_info_args = info_args * len(args)
result = _subclass_of_tuple(args, matched_info_args)
elif (subclass_of(cls_origin, info_generic_type) and cls_args
and len(cls_args) == len(info_args)):
result = all(subclass_of(*tup) for tup in zip(cls_args, info_args))
# Note that issubtype(list, List[...]) is always False.
# Note that the number of arguments must be equal.
return result
def _subclass_of_tuple(
cls_args: typing.Tuple[type, ...],
info_args: typing.Tuple[type, ...]) -> bool:
from typish.functions._get_origin import get_origin
from typish.functions._common_ancestor import common_ancestor_of_types
result = False
if len(info_args) == 2 and info_args[1] is ...:
type_ = get_origin(info_args[0])
if type_ is typing.Union:
# A heterogeneous tuple: check each element if it subclasses the
# union.
result = all([subclass_of(elem, info_args[0]) for elem in cls_args])
else:
result = subclass_of(common_ancestor_of_types(*cls_args), info_args[0])
elif len(cls_args) == len(info_args):
result = all(subclass_of(c1, c2)
for c1, c2 in zip(cls_args, info_args))
return result
def _check_literal(obj: object, func: typing.Callable, *args: type) -> bool:
# Instance or subclass check for Literal.
literal = args[0]
leftovers = args[1:]
literal_args = getattr(literal, '__args__', None)
result = False
if literal_args:
literal_arg = literal_args[0]
result = (obj == literal_arg
and (not leftovers or func(obj, *leftovers)))
return result
def _is_true_case(cls: type, clsinfo: type) -> bool:
# Return whether subclass_of(cls, clsinfo) holds a case that must always be
# True, without the need of further checking.
return cls == clsinfo or cls is Unknown or clsinfo in (typing.Any, object)
def is_issubclass_case(cls: type, clsinfo: type) -> bool:
# Return whether subclass_of(cls, clsinfo) holds a case that can be handled
# by the builtin issubclass.
from typish.functions._is_from_typing import is_from_typing
return (not is_from_typing(clsinfo)
and isinstance(cls, type)
and clsinfo is not type
and '__subclasscheck__' in dir(clsinfo))
def _tuple_args(
cls_args: typing.Iterable[typing.Any]) -> typing.Iterable[type]:
# Get the argument types from a tuple, even if the form is Tuple[int, ...].
result = cls_args
if len(cls_args) > 1 and cls_args[1] is ...:
result = [cls_args[0]]
return result