# SPDX-License-Identifier: GPLv3-or-later
# Copyright © 2025 pygaindalf Rui Pinheiro
import annotationlib
import typing
import warnings
from frozendict import frozendict
from . import script_info
from .classproperty import cached_classproperty
if typing.TYPE_CHECKING:
from .generics import GenericAlias
# MARK: Type aliases
#: Alias for all accepted forms of type hints.
type TypeHint = type | GenericAlias | typing.ForwardRef | typing.TypeAliasType | typing.Union # pyright: ignore[reportInvalidTypeForm]
#: Alias for all forms of resolved types that our methods can return.
type ResolvedType = type | GenericAlias | typing.ForwardRef
# MARK: typing.get_type_hints wrapper
def _get_type_hints(obj: typing.Any, format: annotationlib.Format = annotationlib.Format.FORWARDREF) -> typing.Mapping[str, typing.Any]: # noqa: A002
return frozendict(typing.get_type_hints(obj, format=format))
# MARK: Type hint caching
[docs]
@typing.runtime_checkable
class SupportsCachedTypeHints(typing.Protocol):
@property
def __cached_type_hints__(self) -> typing.Mapping[str, typing.Any]: ...
[docs]
class CachedTypeHintsMixin:
__cached_type_hints_format__ = annotationlib.Format.FORWARDREF
@cached_classproperty
def __cached_type_hints__(cls) -> typing.Mapping[str, typing.Any]:
return _get_type_hints(cls, format=cls.__cached_type_hints_format__)
# MARK: get_type_hints
[docs]
def get_type_hints(obj: typing.Any) -> typing.Mapping[str, typing.Any]:
# Since __cached_type_hints__ is a classproperty, it will not be available during documentation builds
if not script_info.is_documentation_build() and isinstance(obj, SupportsCachedTypeHints):
return obj.__cached_type_hints__
else:
return _get_type_hints(obj)
# MARK: get_type_hint
[docs]
def get_type_hint(obj: typing.Any, attr: str) -> typing.Any | None:
hints = get_type_hints(obj)
return hints.get(attr, None)
# MARK: Union utilities
[docs]
def iterate_type_hints(
hint: TypeHint,
*,
origin: bool = False,
) -> typing.Iterable[ResolvedType]:
from .generics import get_origin
if isinstance(hint, typing.TypeAliasType):
if (evaluate_value := getattr(hint, "evaluate_value", None)) is not None:
hint = annotationlib.call_evaluate_function(evaluate_value, format=annotationlib.Format.FORWARDREF)
else:
msg = f"Cannot iterate over unresolved TypeAlias {hint!r}"
raise TypeError(msg)
if isinstance(hint, typing.ForwardRef):
hint = hint.evaluate(format=annotationlib.Format.FORWARDREF)
if isinstance(hint, typing.ForwardRef):
warnings.warn(f"Cannot iterate {hint!s} type hints, returning as-is", category=UserWarning, stacklevel=2)
return hint
if not isinstance(hint, typing.Union):
if origin:
yield get_origin(hint, passthrough=True)
else:
yield hint
return
for arg in typing.get_args(hint):
if isinstance(arg, typing.Union):
yield from iterate_type_hints(arg, origin=origin)
elif origin:
yield get_origin(arg, passthrough=True)
else:
yield arg
# MARK: Type hint matching
[docs]
def match_type_hint(
typ: type | GenericAlias,
hint: TypeHint,
) -> ResolvedType | None:
from .generics import get_origin
typ_origin = get_origin(typ, passthrough=True)
assert isinstance(typ_origin, type), f"typ_origin must be a type, got {type(typ_origin).__name__}"
if isinstance(hint, typing.ForwardRef):
hint = hint.evaluate(format=annotationlib.Format.FORWARDREF)
if isinstance(hint, typing.ForwardRef):
warnings.warn(f"Cannot match {hint!s} type hints, returning as-is", category=UserWarning, stacklevel=2)
return hint
for arg in iterate_type_hints(hint):
arg_origin = get_origin(arg, passthrough=True)
if not isinstance(arg_origin, type):
msg = f"arg_origin must be a type, got {type(arg_origin).__name__}"
raise TypeError(msg)
if issubclass(typ_origin, arg_origin):
return arg
return None
[docs]
def match_type_hints(
hint_a: TypeHint,
hint_b: TypeHint,
) -> ResolvedType | None:
for a in iterate_type_hints(hint_a):
if match_type_hint(a, hint_b) is not None:
return a
return None
[docs]
def validate_type_hint(typ: type, hint: type | GenericAlias | typing.ForwardRef) -> bool:
return match_type_hint(typ, hint) is not None