Module arti.internal.type_hints
None
None
View Source
from __future__ import annotations
import inspect
import sys
import types
from collections.abc import Callable
from datetime import date, datetime
from typing import (
TYPE_CHECKING,
Annotated,
Any,
Literal,
Optional,
TypeVar,
Union,
cast,
get_args,
get_origin,
get_type_hints,
overload,
)
_T = TypeVar("_T")
NoneType = cast(type, type(None)) # mypy otherwise treats type(None) as an object
def _check_issubclass(klass: Any, check_type: type) -> bool:
# If a hint is Annotated, we want to unwrap the underlying type and discard the rest of the
# metadata.
klass = discard_Annotated(klass)
klass_origin, klass_args = get_origin(klass), get_args(klass)
if isinstance(klass, TypeVar):
klass = cast(type, Any) if klass.__bound__ is None else klass.__bound__
klass_origin, klass_args = get_origin(klass), get_args(klass)
check_type_origin, check_type_args = get_origin(check_type), get_args(check_type)
if check_type_origin is Annotated:
check_type = check_type_args[0]
check_type_origin, check_type_args = get_origin(check_type), get_args(check_type)
if isinstance(check_type, TypeVar):
check_type = cast(type, Any) if check_type.__bound__ is None else check_type.__bound__
check_type_origin, check_type_args = get_origin(check_type), get_args(check_type)
if klass is Any:
return check_type is Any
if check_type is Any:
return True
if check_type is None:
return klass is NoneType
# eg: issubclass(tuple, tuple)
if klass_origin is None and check_type_origin is None:
return issubclass(klass, check_type)
# eg: issubclass(tuple[int], tuple)
if klass_origin is not None and check_type_origin is None:
return issubclass(klass_origin, check_type)
# eg: issubclass(tuple, tuple[int])
if klass_origin is None and check_type_origin is not None:
return issubclass(klass, check_type_origin) and not check_type_args
# eg: issubclass(tuple[int], tuple[int])
if klass_origin is not None and check_type_origin is not None:
# NOTE: Considering all container types covariant for simplicity (mypy may be more strict).
#
# The builtin mutable containers (list, dict, etc) are invariant (klass_args ==
# check_type_args), but the interfaces (Mapping, Sequence, etc) and immutable containers are
# covariant.
if check_type_args and not (
len(klass_args) == len(check_type_args)
and all(
# check subclass OR things like "..."
lenient_issubclass(klass_arg, check_type_arg) or klass_arg is check_type_arg
for (klass_arg, check_type_arg) in zip(klass_args, check_type_args)
)
):
return False
return lenient_issubclass(klass_origin, check_type_origin)
# Shouldn't happen, but need to explicitly say "x is not None" to narrow mypy types.
raise NotImplementedError("The origin conditions don't cover all cases!")
def discard_Annotated(type_: Any) -> Any:
return get_args(type_)[0] if is_Annotated(type_) else type_
def get_class_type_vars(klass: type) -> tuple[type, ...]:
"""Get the bound type variables from a class
NOTE: Only vars from the *first* Generic in the mro *with all variables bound* will be returned.
"""
bases = (klass,) if is_generic_alias(klass) else klass.__orig_bases__ # type: ignore[attr-defined]
for base in bases:
base_origin = get_origin(base)
if base_origin is None:
continue
args = get_args(base)
if any(isinstance(arg, TypeVar) for arg in args):
continue
return args
raise TypeError(f"{klass.__name__} must subclass a subscripted Generic")
@overload
def get_item_from_annotated(
annotation: Any, klass: type[_T], *, is_subclass: Literal[True]
) -> Optional[type[_T]]:
...
@overload
def get_item_from_annotated(
annotation: Any, klass: type[_T], *, is_subclass: Literal[False]
) -> Optional[_T]:
...
@overload
def get_item_from_annotated(
annotation: Any, klass: type[_T], *, is_subclass: bool
) -> Optional[Union[_T, type[_T]]]:
...
def get_item_from_annotated(
annotation: Any, klass: type[_T], *, is_subclass: bool
) -> Optional[Union[_T, type[_T]]]:
from arti.internal.utils import one_or_none
if not is_Annotated(annotation):
return None
_, *hints = get_args(annotation)
checker = lenient_issubclass if is_subclass else isinstance
return one_or_none([hint for hint in hints if checker(hint, klass)], item_name=klass.__name__)
def get_annotation_from_value(value: Any) -> Any:
if value is None:
return None
if isinstance(value, (bool, bytes, date, datetime, float, int, str)):
return type(value)
if isinstance(value, (tuple, list, set, frozenset)):
first, *tail = tuple(value)
first_type = type(first)
if all(isinstance(v, first_type) for v in tail):
if isinstance(value, tuple):
return tuple[first_type, ...] # type: ignore[valid-type]
return type(value)[first_type] # type: ignore[index]
if isinstance(value, dict):
items = value.items()
first_key_type, first_value_type = (type(v) for v in tuple(items)[0])
if all(
isinstance(k, first_key_type) and isinstance(v, first_value_type) for (k, v) in items
):
return dict[first_key_type, first_value_type] # type: ignore[valid-type]
# TODO: Implement with TypedDict to support Struct types...?
raise NotImplementedError(f"Unable to determine type of {value}")
def lenient_issubclass(klass: Any, class_or_tuple: Union[type, tuple[type, ...]]) -> bool:
if not (
isinstance(klass, (type, types.GenericAlias, TypeVar))
or is_Annotated(klass)
or klass is Any
):
return False
if isinstance(class_or_tuple, tuple):
return any(lenient_issubclass(klass, subtype) for subtype in class_or_tuple)
check_type = class_or_tuple
# NOTE: py 3.10 supports issubclass with Unions (eg: `issubclass(str, str | int)`)
if is_union_hint(check_type):
return any(lenient_issubclass(klass, subtype) for subtype in get_args(check_type))
return _check_issubclass(klass, check_type)
def _tidy_return(return_annotation: Any, *, force_tuple_return: bool) -> Any:
if not force_tuple_return:
return return_annotation
if lenient_issubclass(get_origin(return_annotation), tuple):
return get_args(return_annotation)
return (return_annotation,)
def tidy_signature(
fn: Callable[..., Any],
sig: inspect.Signature,
*,
force_tuple_return: bool = False,
remove_owner: bool = False,
) -> inspect.Signature:
type_hints = get_type_hints(fn, include_extras=True)
sig = sig.replace(return_annotation=type_hints.get("return", sig.return_annotation))
return sig.replace(
parameters=[
p.replace(annotation=type_hints.get(p.name, p.annotation))
for p in sig.parameters.values()
if (p.name not in ("cls", "self") if remove_owner else True)
],
return_annotation=(
sig.empty
if sig.return_annotation is sig.empty
else _tidy_return(sig.return_annotation, force_tuple_return=force_tuple_return)
),
)
def signature(
fn: Callable[..., Any],
*,
follow_wrapped: bool = True,
force_tuple_return: bool = False,
remove_owner: bool = True,
) -> inspect.Signature:
"""Convenience wrapper around `inspect.signature`.
The returned Signature will have `cls`/`self` parameters removed if `remove_owner` is `True` and
`tuple[...]` converted to `tuple(...)` in the `return_annotation`.
"""
return tidy_signature(
fn=fn,
sig=inspect.signature(fn, follow_wrapped=follow_wrapped),
force_tuple_return=force_tuple_return,
remove_owner=remove_owner,
)
#############################################
# Helpers for typing across python versions #
#############################################
#
# Focusing on 3.9+ (for now)
if sys.version_info < (3, 11): # pragma: no cover
from typing_extensions import Self as Self
else: # pragma: no cover
from typing import Self as Self
if sys.version_info < (3, 10): # pragma: no cover
def is_union(type_: Any) -> bool:
return type_ is Union
def is_typeddict(type_: Any) -> bool:
# mypy doesn't know of typing._TypedDictMeta, but `type: ignore` would be "unused" (and error)
# on other python versions.
if TYPE_CHECKING:
from typing import _TypedDict as _TypedDictMeta
else:
from typing import _TypedDictMeta
return isinstance(type_, _TypedDictMeta)
else: # pragma: no cover
from typing import is_typeddict as is_typeddict # noqa: F401
# mypy doesn't know of types.UnionType yet, but `type: ignore` would be "unused"
# (and error) on other python versions.
def is_union(type_: Any) -> bool:
# `Union[int, str]` or `int | str`
return type_ is Union or type_ is types.UnionType # noqa: E721
def is_Annotated(type_: Any) -> bool:
return get_origin(type_) is Annotated
def is_generic_alias(type_: Any) -> bool:
from typing import _GenericAlias # type: ignore[attr-defined]
return isinstance(type_, (_GenericAlias, types.GenericAlias))
def is_optional_hint(type_: Any) -> bool:
# Optional[x] is represented as Union[x, NoneType]
return is_union(get_origin(type_)) and NoneType in get_args(type_)
def is_union_hint(type_: Any) -> bool:
return get_origin(type_) is Union
Variables
TYPE_CHECKING
Functions
discard_Annotated
def discard_Annotated(
type_: 'Any'
) -> 'Any'
View Source
def discard_Annotated(type_: Any) -> Any:
return get_args(type_)[0] if is_Annotated(type_) else type_
get_annotation_from_value
def get_annotation_from_value(
value: 'Any'
) -> 'Any'
View Source
def get_annotation_from_value(value: Any) -> Any:
if value is None:
return None
if isinstance(value, (bool, bytes, date, datetime, float, int, str)):
return type(value)
if isinstance(value, (tuple, list, set, frozenset)):
first, *tail = tuple(value)
first_type = type(first)
if all(isinstance(v, first_type) for v in tail):
if isinstance(value, tuple):
return tuple[first_type, ...] # type: ignore[valid-type]
return type(value)[first_type] # type: ignore[index]
if isinstance(value, dict):
items = value.items()
first_key_type, first_value_type = (type(v) for v in tuple(items)[0])
if all(
isinstance(k, first_key_type) and isinstance(v, first_value_type) for (k, v) in items
):
return dict[first_key_type, first_value_type] # type: ignore[valid-type]
# TODO: Implement with TypedDict to support Struct types...?
raise NotImplementedError(f"Unable to determine type of {value}")
get_class_type_vars
def get_class_type_vars(
klass: 'type'
) -> 'tuple[type, ...]'
Get the bound type variables from a class
NOTE: Only vars from the first Generic in the mro with all variables bound will be returned.
View Source
def get_class_type_vars(klass: type) -> tuple[type, ...]:
"""Get the bound type variables from a class
NOTE: Only vars from the *first* Generic in the mro *with all variables bound* will be returned.
"""
bases = (klass,) if is_generic_alias(klass) else klass.__orig_bases__ # type: ignore[attr-defined]
for base in bases:
base_origin = get_origin(base)
if base_origin is None:
continue
args = get_args(base)
if any(isinstance(arg, TypeVar) for arg in args):
continue
return args
raise TypeError(f"{klass.__name__} must subclass a subscripted Generic")
get_item_from_annotated
def get_item_from_annotated(
annotation: 'Any',
klass: 'type[_T]',
*,
is_subclass: 'bool'
) -> 'Optional[Union[_T, type[_T]]]'
View Source
def get_item_from_annotated(
annotation: Any, klass: type[_T], *, is_subclass: bool
) -> Optional[Union[_T, type[_T]]]:
from arti.internal.utils import one_or_none
if not is_Annotated(annotation):
return None
_, *hints = get_args(annotation)
checker = lenient_issubclass if is_subclass else isinstance
return one_or_none([hint for hint in hints if checker(hint, klass)], item_name=klass.__name__)
is_Annotated
def is_Annotated(
type_: 'Any'
) -> 'bool'
View Source
def is_Annotated(type_: Any) -> bool:
return get_origin(type_) is Annotated
is_generic_alias
def is_generic_alias(
type_: 'Any'
) -> 'bool'
View Source
def is_generic_alias(type_: Any) -> bool:
from typing import _GenericAlias # type: ignore[attr-defined]
return isinstance(type_, (_GenericAlias, types.GenericAlias))
is_optional_hint
def is_optional_hint(
type_: 'Any'
) -> 'bool'
View Source
def is_optional_hint(type_: Any) -> bool:
# Optional[x] is represented as Union[x, NoneType]
return is_union(get_origin(type_)) and NoneType in get_args(type_)
is_union
def is_union(
type_: 'Any'
) -> 'bool'
View Source
def is_union(type_: Any) -> bool:
# `Union[int, str]` or `int | str`
return type_ is Union or type_ is types.UnionType # noqa: E721
is_union_hint
def is_union_hint(
type_: 'Any'
) -> 'bool'
View Source
def is_union_hint(type_: Any) -> bool:
return get_origin(type_) is Union
lenient_issubclass
def lenient_issubclass(
klass: 'Any',
class_or_tuple: 'Union[type, tuple[type, ...]]'
) -> 'bool'
View Source
def lenient_issubclass(klass: Any, class_or_tuple: Union[type, tuple[type, ...]]) -> bool:
if not (
isinstance(klass, (type, types.GenericAlias, TypeVar))
or is_Annotated(klass)
or klass is Any
):
return False
if isinstance(class_or_tuple, tuple):
return any(lenient_issubclass(klass, subtype) for subtype in class_or_tuple)
check_type = class_or_tuple
# NOTE: py 3.10 supports issubclass with Unions (eg: `issubclass(str, str | int)`)
if is_union_hint(check_type):
return any(lenient_issubclass(klass, subtype) for subtype in get_args(check_type))
return _check_issubclass(klass, check_type)
signature
def signature(
fn: 'Callable[..., Any]',
*,
follow_wrapped: 'bool' = True,
force_tuple_return: 'bool' = False,
remove_owner: 'bool' = True
) -> 'inspect.Signature'
Convenience wrapper around inspect.signature
.
The returned Signature will have cls
/self
parameters removed if remove_owner
is True
and
tuple[...]
converted to tuple(...)
in the return_annotation
.
View Source
def signature(
fn: Callable[..., Any],
*,
follow_wrapped: bool = True,
force_tuple_return: bool = False,
remove_owner: bool = True,
) -> inspect.Signature:
"""Convenience wrapper around `inspect.signature`.
The returned Signature will have `cls`/`self` parameters removed if `remove_owner` is `True` and
`tuple[...]` converted to `tuple(...)` in the `return_annotation`.
"""
return tidy_signature(
fn=fn,
sig=inspect.signature(fn, follow_wrapped=follow_wrapped),
force_tuple_return=force_tuple_return,
remove_owner=remove_owner,
)
tidy_signature
def tidy_signature(
fn: 'Callable[..., Any]',
sig: 'inspect.Signature',
*,
force_tuple_return: 'bool' = False,
remove_owner: 'bool' = False
) -> 'inspect.Signature'
View Source
def tidy_signature(
fn: Callable[..., Any],
sig: inspect.Signature,
*,
force_tuple_return: bool = False,
remove_owner: bool = False,
) -> inspect.Signature:
type_hints = get_type_hints(fn, include_extras=True)
sig = sig.replace(return_annotation=type_hints.get("return", sig.return_annotation))
return sig.replace(
parameters=[
p.replace(annotation=type_hints.get(p.name, p.annotation))
for p in sig.parameters.values()
if (p.name not in ("cls", "self") if remove_owner else True)
],
return_annotation=(
sig.empty
if sig.return_annotation is sig.empty
else _tidy_return(sig.return_annotation, force_tuple_return=force_tuple_return)
),
)
Classes
NoneType
class NoneType(
/,
*args,
**kwargs
)