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,39 @@
from typish._meta import __version__
from typish._types import (
T,
KT,
VT,
Empty,
Unknown,
Module,
NoneType,
Ellipsis_,
EllipsisType,
)
from typish.classes._cls_dict import ClsDict
from typish.classes._cls_function import ClsFunction
from typish.classes._literal import Literal, LiteralAlias, is_literal_type
from typish.classes._something import Something, TypingType
from typish.classes._subscriptable_type import SubscriptableType
from typish.classes._union_type import UnionType
from typish.decorators._hintable import hintable
from typish.functions._common_ancestor import (
common_ancestor,
common_ancestor_of_types
)
from typish.functions._get_alias import get_alias
from typish.functions._get_args import get_args
from typish.functions._get_mro import get_mro
from typish.functions._get_origin import get_origin
from typish.functions._get_simple_name import get_simple_name
from typish.functions._get_type import get_type
from typish.functions._get_type_hints_of_callable import (
get_args_and_return_type,
get_type_hints_of_callable
)
from typish.functions._instance_of import instance_of
from typish.functions._is_type_annotation import is_type_annotation
from typish.functions._is_optional_type import is_optional_type
from typish.functions._subclass_of import subclass_of
from typish.functions._is_from_typing import is_from_typing
from typish._state import State, register_get_type

View File

@@ -0,0 +1,7 @@
__title__ = 'typish'
__version__ = '1.9.3'
__author__ = 'Ramon Hagenaars'
__author_email__ = 'ramon.hagenaars@gmail.com'
__description__ = 'Functionality for types'
__url__ = 'https://github.com/ramonhagenaars/typish'
__license__ = 'MIT'

View File

@@ -0,0 +1,45 @@
from typing import Callable
from typish import T
class State:
"""
A class which instances hold any state that may be used by typish.
"""
def __init__(self) -> None:
"""
Constructor.
"""
self.get_type_per_cls = {}
def register_get_type(
self,
cls: T,
get_type_function: Callable[[T], type]) -> None:
"""
Register a callable for some type that is to be used when calling
typish.get_type.
:param cls: the type for which that given callable is to be called.
:param get_type_function: the callable to call for that type.
:return: None.
"""
self.get_type_per_cls[cls] = get_type_function
DEFAULT = State()
def register_get_type(
cls: T,
get_type_function: Callable[[T], type],
state: State = DEFAULT) -> None:
"""
Register a callable for some type that is to be used when calling
typish.get_type.
:param cls: the type for which that given callable is to be called.
:param get_type_function: the callable to call for that type.
:param state: any state that is used by typish.
:return: None.
"""
state.register_get_type(cls, get_type_function)

View File

@@ -0,0 +1,18 @@
"""
PRIVATE MODULE: do not import (from) it directly.
This module contains types that are not available by default.
"""
import typing
from inspect import Parameter
T = typing.TypeVar('T')
KT = typing.TypeVar('KT')
VT = typing.TypeVar('VT')
Empty = Parameter.empty
Unknown = type('Unknown', (Empty, ), {})
Module = type(typing)
NoneType = type(None)
Ellipsis_ = type(...) # Use EllipsisType instead.
EllipsisType = type(...)

View File

@@ -0,0 +1,49 @@
from collections import OrderedDict
from typing import Optional, Any
class ClsDict(OrderedDict):
"""
ClsDict is a dict that accepts (only) types as keys and will return its
values depending on instance checks rather than equality checks.
"""
def __new__(cls, *args, **kwargs):
"""
Construct a new instance of ``ClsDict``.
:param args: a dict.
:param kwargs: any kwargs that ``dict`` accepts.
:return: a ``ClsDict``.
"""
from typish.functions._is_type_annotation import is_type_annotation
if len(args) > 1:
raise TypeError('TypeDict accepts only one positional argument, '
'which must be a dict.')
if args and not isinstance(args[0], dict):
raise TypeError('TypeDict accepts only a dict as positional '
'argument.')
if not all([is_type_annotation(key) for key in args[0]]):
raise TypeError('The given dict must only hold types as keys.')
return super().__new__(cls, args[0], **kwargs)
def __getitem__(self, item: Any) -> Any:
"""
Return the value of the first encounter of a key for which
``is_instance(item, key)`` holds ``True``.
:param item: any item.
:return: the value of which the type corresponds with item.
"""
from typish.functions._get_type import get_type
from typish.functions._subclass_of import subclass_of
item_type = get_type(item, use_union=True)
for key, value in self.items():
if subclass_of(item_type, key):
return value
raise KeyError('No match for {}'.format(item))
def get(self, item: Any, default: Any = None) -> Optional[Any]:
try:
return self.__getitem__(item)
except KeyError:
return default

View File

@@ -0,0 +1,71 @@
import inspect
from collections import OrderedDict
from typing import Callable, Any, Union, Iterable, Dict, Tuple
from typish._types import Empty
from typish.classes._cls_dict import ClsDict
class ClsFunction:
"""
ClsDict is a callable that takes a ClsDict or a dict. When called, it uses
the first argument to check for the right function in its body, executes it
and returns the result.
"""
def __init__(self, body: Union[ClsDict,
Dict[type, Callable],
Iterable[Tuple[type, Callable]],
Iterable[Callable]]):
from typish.functions._instance_of import instance_of
if isinstance(body, ClsDict):
self.body = body
elif isinstance(body, dict):
self.body = ClsDict(body)
elif instance_of(body, Iterable[Callable]):
list_of_tuples = []
for func in body:
signature = inspect.signature(func)
params = list(signature.parameters.keys())
if not params:
raise TypeError('ClsFunction expects callables that take '
'at least one parameter, {} does not.'
.format(func.__name__))
first_param = signature.parameters[params[0]]
hint = first_param.annotation
key = Any if hint == Empty else hint
list_of_tuples.append((key, func))
self.body = ClsDict(OrderedDict(list_of_tuples))
elif instance_of(body, Iterable[Tuple[type, Callable]]):
self.body = ClsDict(OrderedDict(body))
else:
raise TypeError('ClsFunction expects a ClsDict or a dict that can '
'be turned to a ClsDict or an iterable of '
'callables.')
if not all(isinstance(value, Callable) for value in self.body.values()):
raise TypeError('ClsFunction expects a dict or ClsDict with only '
'callables as values.')
def understands(self, item: Any) -> bool:
"""
Check to see if this ClsFunction can take item.
:param item: the item that is checked.
:return: True if this ClsFunction can take item.
"""
try:
self.body[item]
return True
except KeyError:
return False
def __call__(self, *args, **kwargs):
if not args:
raise TypeError('ClsFunction must be called with at least 1 '
'positional argument.')
callable_ = self.body[args[0]]
try:
return callable_(*args, **kwargs)
except TypeError as err:
raise TypeError('Unable to call function for \'{}\': {}'
.format(args[0], err.args[0]))

View File

@@ -0,0 +1,74 @@
import typing
from typish.classes._subscriptable_type import SubscriptableType
def is_literal_type(cls: typing.Any) -> bool:
"""
Return whether cls is a Literal type.
:param cls: the type that is to be checked.
:return: True if cls is a Literal type.
"""
from typish.functions._get_simple_name import get_simple_name
return get_simple_name(cls) == 'Literal'
class _LiteralMeta(SubscriptableType):
"""
A Metaclass that exists to serve Literal and alter the __args__ attribute.
"""
def __getattribute__(cls, item):
"""
This method makes sure that __args__ is a tuple, like with
typing.Literal.
:param item: the name of the attribute that is obtained.
:return: the attribute.
"""
if item == '__args__':
try:
result = SubscriptableType.__getattribute__(cls, item)
if (result and isinstance(result, tuple)
and isinstance(result[0], tuple)):
result = result[0] # result was a tuple in a tuple.
if result and not isinstance(result, tuple):
result = (result,)
except AttributeError: # pragma: no cover
# In case of Python 3.5
result = tuple()
elif item in ('__origin__', '__name__', '_name'):
result = 'Literal'
else:
result = SubscriptableType.__getattribute__(cls, item)
return result
def __instancecheck__(self, instance):
return self.__args__ and instance in self.__args__
def __str__(self):
args = ', '.join(str(arg) for arg in self.__args__)
return '{}[{}]'.format(self.__name__, args)
def __subclasscheck__(self, subclass: typing.Any) -> bool:
return is_literal_type(subclass)
class LiteralAlias(type, metaclass=_LiteralMeta):
"""
This is a backwards compatible variant of typing.Literal (Python 3.8+).
"""
@staticmethod
def from_literal(literal: typing.Any) -> typing.Type['LiteralAlias']:
"""
Create a LiteralAlias from the given typing.Literal.
:param literal: the typing.Literal type.
:return: a LiteralAlias type.
"""
from typish.functions._get_args import get_args
args = get_args(literal)
return LiteralAlias[args] if args else LiteralAlias
# If Literal is available (Python 3.8+), then return that type instead.
Literal = getattr(typing, 'Literal', LiteralAlias)

View File

@@ -0,0 +1,122 @@
import types
from collections import OrderedDict
from typing import Any, Dict, Callable, Tuple
from typish.classes._subscriptable_type import SubscriptableType
class _SomethingMeta(SubscriptableType):
"""
This metaclass is coupled to ``Something``.
"""
def __instancecheck__(self, instance: object) -> bool:
# Check if all attributes from self.signature are also present in
# instance and also check that their types correspond.
from typish.functions._instance_of import instance_of
sig = self.signature()
for key in sig:
attr = getattr(instance, key, None)
if not attr or not instance_of(attr, sig[key]):
return False
return True
def __subclasscheck__(self, subclass: type) -> bool:
# If an instance of type subclass is an instance of self, then subclass
# is a sub class of self.
from typish.functions._subclass_of import subclass_of
from typish.functions._get_type_hints_of_callable import get_args_and_return_type
self_sig = self.signature()
other_sig = Something.like(subclass).signature()
for attr in self_sig:
if attr in other_sig:
attr_sig = other_sig[attr]
if (not isinstance(subclass.__dict__[attr], staticmethod)
and not isinstance(subclass.__dict__[attr], classmethod)
and subclass_of(attr_sig, Callable)):
# The attr must be a regular method or class method, so the
# first parameter should be ignored.
args, rt = get_args_and_return_type(attr_sig)
attr_sig = Callable[list(args[1:]), rt]
if not subclass_of(attr_sig, self_sig[attr]):
return False
return True
def __eq__(self, other: 'Something') -> bool:
return (isinstance(other, _SomethingMeta)
and self.signature() == other.signature())
def __repr__(self):
sig = self.signature()
sig_ = ', '.join(["'{}': {}".format(k, self._type_repr(sig[k]))
for k in sig])
return 'typish.Something[{}]'.format(sig_)
def __hash__(self):
# This explicit super call is required for Python 3.5 and 3.6.
return super.__hash__(self)
def _type_repr(self, obj):
"""Return the repr() of an object, special-casing types (internal helper).
If obj is a type, we return a shorter version than the default
type.__repr__, based on the module and qualified name, which is
typically enough to uniquely identify a type. For everything
else, we fall back on repr(obj).
"""
if isinstance(obj, type) and not issubclass(obj, Callable):
return obj.__qualname__
if obj is ...:
return '...'
if isinstance(obj, types.FunctionType):
return obj.__name__
return repr(obj)
class Something(type, metaclass=_SomethingMeta):
"""
This class allows one to define an interface for something that has some
attributes, such as objects or classes or maybe even modules.
"""
@classmethod
def signature(mcs) -> Dict[str, type]:
"""
Return the signature of this ``Something`` as a dict.
:return: a dict with attribute names as keys and types as values.
"""
result = OrderedDict()
args = mcs.__args__
if isinstance(mcs.__args__, slice):
args = (mcs.__args__,)
arg_keys = sorted(args)
if isinstance(mcs.__args__, dict):
for key in arg_keys:
result[key] = mcs.__args__[key]
else:
for slice_ in arg_keys:
result[slice_.start] = slice_.stop
return result
def __getattr__(cls, item):
# This method exists solely to fool the IDE into believing that
# Something can have any attribute.
return type.__getattr__(cls, item) # pragma: no cover
@staticmethod
def like(obj: Any, exclude_privates: bool = True) -> 'Something':
"""
Return a ``Something`` for the given ``obj``.
:param obj: the object of which a ``Something`` is to be made.
:param exclude_privates: if ``True``, private variables are excluded.
:return: a ``Something`` that corresponds to ``obj``.
"""
from typish.functions._get_type import get_type
signature = {attr: get_type(getattr(obj, attr)) for attr in dir(obj)
if not exclude_privates or not attr.startswith('_')}
return Something[signature]
TypingType = Something['__origin__': type, '__args__': Tuple[type, ...]]

View File

@@ -0,0 +1,50 @@
class _SubscribedType(type):
"""
This class is a placeholder to let the IDE know the attributes of the
returned type after a __getitem__.
"""
__origin__ = None
__args__ = None
class SubscriptableType(type):
"""
This metaclass will allow a type to become subscriptable.
>>> class SomeType(metaclass=SubscriptableType):
... pass
>>> SomeTypeSub = SomeType['some args']
>>> SomeTypeSub.__args__
'some args'
>>> SomeTypeSub.__origin__.__name__
'SomeType'
"""
def __init_subclass__(mcs, **kwargs):
mcs._hash = None
mcs.__args__ = None
mcs.__origin__ = None
def __getitem__(self, item) -> _SubscribedType:
body = {
**self.__dict__,
'__args__': item,
'__origin__': self,
}
bases = self, *self.__bases__
result = type(self.__name__, bases, body)
if hasattr(result, '_after_subscription'):
# TODO check if _after_subscription is static
result._after_subscription(item)
return result
def __eq__(self, other):
self_args = getattr(self, '__args__', None)
self_origin = getattr(self, '__origin__', None)
other_args = getattr(other, '__args__', None)
other_origin = getattr(other, '__origin__', None)
return self_args == other_args and self_origin == other_origin
def __hash__(self):
if not getattr(self, '_hash', None):
self._hash = hash('{}{}'.format(self.__origin__, self.__args__))
return self._hash

View File

@@ -0,0 +1,7 @@
class _UnionType(type):
def __instancecheck__(self, instance):
return str(instance).startswith('typing.Union')
class UnionType(type, metaclass=_UnionType):
...

View File

@@ -0,0 +1,109 @@
import inspect
import re
from functools import wraps
from typing import Dict, Optional, Callable, List
_DEFAULT_PARAM_NAME = 'hint'
class _Hintable:
_hints_per_frame = {}
def __init__(
self,
decorated: Callable,
param: str,
stack_index: int) -> None:
self._decorated = decorated
self._param = param
self._stack_index = stack_index
def __call__(self, *args, **kwargs):
stack = inspect.stack()
previous_frame = stack[self._stack_index]
frame_id = id(previous_frame.frame)
if not self._hints_per_frame.get(frame_id):
code_context = previous_frame.code_context[0].strip()
hint_strs = self._extract_hints(code_context)
globals_ = previous_frame.frame.f_globals
# Store the type hint if any, otherwise the string, otherwise None.
hints = [self._to_cls(hint_str, globals_) or hint_str or None
for hint_str in hint_strs]
self._hints_per_frame[frame_id] = hints
hint = (self._hints_per_frame.get(frame_id) or [None]).pop()
kwargs_ = {**kwargs, self._param: kwargs.get(self._param, hint)}
return self._decorated(*args, **kwargs_)
def _extract_hints(self, code_context: str) -> List[str]:
result = []
regex = (
r'.+?(:(.+?))?=\s*' # e.g. 'x: int = ', $2 holds hint
r'.*?{}\s*\(.*?\)\s*' # e.g. 'func(...) '
r'(#\s*type\s*:\s*(\w+))?\s*' # e.g. '# type: int ', $4 holds hint
).format(self._decorated.__name__)
# Find all matches and store them (reversed) in the resulting list.
for _, group2, _, group4 in re.findall(regex, code_context):
raw_hint = (group2 or group4).strip()
if self._is_between(raw_hint, '\'') or self._is_between(raw_hint, '"'):
# Remove any quotes that surround the hint.
raw_hint = raw_hint[1:-1].strip()
result.insert(0, raw_hint)
return result
def _is_between(self, subject: str, character: str) -> bool:
return subject.startswith(character) and subject.endswith(character)
def _to_cls(self, hint: str, f_globals: Dict[str, type]) -> Optional[type]:
return __builtins__.get(hint, f_globals.get(hint))
def _get_wrapper(decorated, param: str, stack_index: int):
@wraps(decorated)
def _wrapper(*args, **kwargs):
return _Hintable(decorated, param, stack_index)(*args, **kwargs)
if isinstance(decorated, type):
raise TypeError('Only functions and methods should be decorated with '
'\'hintable\', not classes.')
if param not in inspect.signature(decorated).parameters:
raise TypeError('The decorated \'{}\' must accept a parameter with '
'the name \'{}\'.'
.format(decorated.__name__, param))
return _wrapper
def hintable(decorated=None, *, param: str = _DEFAULT_PARAM_NAME) -> Callable:
"""
Allow a function or method to receive the type hint of a receiving
variable.
Example:
>>> @hintable
... def cast(value, hint):
... return hint(value)
>>> x: int = cast('42')
42
Use this decorator wisely. If a variable was hinted with a type (e.g. int
in the above example), your function should return a value of that type
(in the above example, that would be an int value).
:param decorated: a function or method.
:param param: the name of the parameter that receives the type hint.
:return: the decorated function/method wrapped into a new function.
"""
if decorated is not None:
wrapper = _get_wrapper(decorated, param, 2)
else:
wrapper = lambda decorated_: _get_wrapper(decorated_, param, 2)
return wrapper

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