Skip to content

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
)