__all__ = (
"PromisedType",
"ModuleType",
"type_mapping",
"resolve_type_hint",
"is_faithful",
)
import abc
import sys
import typing
import warnings
from collections.abc import Callable, Hashable
from functools import reduce
from operator import or_
from types import UnionType
from typing import Literal, TypeGuard, TypeVar, cast, final, get_args, get_origin
from beartype.typing import Protocol
from beartype.vale._core._valecore import BeartypeValidator
T = TypeVar("T", bound="ResolvableType")
class ResolvableType(type):
"""A resolvable type that will resolve to `type` after `type` has been delivered via
:meth:`.ResolvableType.deliver`. Before then, it will resolve to itself.
Args:
name (str): Name of the type to be delivered.
"""
def __init__(self, name: str, /) -> None:
type.__init__(self, name, (), {})
self._type: type | None = None
def __new__(cls: type[T], name: str) -> T:
return type.__new__(cls, name, (), {})
def deliver(self: T, delivered_type: type, /) -> T:
"""Deliver the type.
Args:
delivered_type (type): Type to deliver.
Returns:
:class:`ResolvableType`: `self`.
"""
self._type = delivered_type
return self
def resolve(self: T) -> type | T:
"""Resolve the type.
Returns:
type: If no type has been delivered, this will return itself. If a type
`type` has been delivered via :meth:`.ResolvableType.deliver`, this will
return that type.
"""
return self if self._type is None else self._type
[docs]
@final
class PromisedType(ResolvableType):
"""A type that is promised to be available when you will you need it.
Args:
name (str, optional): Name of the type that is promised. Defaults to
`"SomeType"`.
"""
def __init__(self, name: str = "SomeType") -> None:
ResolvableType.__init__(self, f"PromisedType[{name}]")
self._name = name
def __new__(cls, name: str = "SomeType") -> "PromisedType":
return super().__new__(cls, f"PromisedType[{name}]")
def __repr__(self) -> str:
return f"<class 'plum.PromisedType[{self._name}]'>"
TModuleType = TypeVar("TModuleType", bound="ModuleType")
[docs]
@final
class ModuleType(ResolvableType):
"""A type from another module.
Args:
module (str): Module that the type lives in.
name (str): Name of the type that is promised.
allow_fail (bool, optional): If the type is does not exist in `module`,
do not raise an `AttributeError`.
condition (Callable[[], bool], optional): A callable that can check a condition,
like a package version. This callable will be run whenever `module` has been
imported. Only if the callable returns `True`, `name` will be imported
from `module`.
faithful (bool, optional): If set, set the dunder `__faithful__` of the type to
this value upon retrieval.
"""
def __init__(
self,
module: str,
name: str,
*,
allow_fail: bool = False,
condition: Callable[[], bool] | None = None,
faithful: bool | None = None,
) -> None:
if module in {"__builtin__", "__builtins__"}:
module = "builtins"
super().__init__(f"ModuleType[{module}.{name}]")
self._name = name
self._module = module
self._allow_fail = allow_fail
self._condition = condition
self._faithful = faithful
def __new__(
cls: type[TModuleType], module: str, name: str, **kwargs: object
) -> TModuleType:
return ResolvableType.__new__(cls, f"ModuleType[{module}.{name}]")
[docs]
def deliver(self: TModuleType, delivered_type: type, /) -> TModuleType:
return_value = super().deliver(delivered_type)
if self._faithful is not None:
# Only set `delivered_type.__faithful__` if it is not already set to a
# different value.
if (
# Use `hasattr` instead of `_has_dunder_faithful` so `mypy` remains
# aware that `delivered_type` is a `type` and won't complain about
# `delivered_type.__name__`.
hasattr(delivered_type, "__faithful__")
and delivered_type.__faithful__ != self._faithful
):
raise TypeError(
f"`{delivered_type.__name__}.__faithful__` is already set and "
f"would be changed by `{self.__name__}` to a different value."
)
delivered_type.__faithful__ = self._faithful # type: ignore[attr-defined]
return return_value
[docs]
def retrieve(self) -> bool:
"""Attempt to retrieve the type from the reference module.
Returns:
bool: Whether the retrieval succeeded.
"""
if self._type is None and self._module in sys.modules:
# If a condition is given, check the condition before attempting to import.
if self._condition is not None and not self._condition():
return False
retrieved: object = sys.modules[self._module]
for name in self._name.split("."):
# If `retrieved` does not contain `name` and `self._allow_fail` is
# set, then silently fail.
if not hasattr(retrieved, name) and self._allow_fail:
return False
retrieved = getattr(retrieved, name)
# We expect this to be a type, so we cast it.
self.deliver(cast(type, retrieved))
return self._type is not None
def _is_hint(x: object) -> bool:
"""Check if an object is a type hint.
Args:
x (object): Object.
Returns:
bool: `True` if `x` is a type hint and `False` otherwise.
"""
try:
if x.__module__ == "builtins":
# Check if `x` is a subscripted built-in. We do this by checking the module
# of the type of `x`.
x = type(x)
return x.__module__ in {
"types", # E.g., `tuple[int]`
"typing",
"collections.abc", # E.g., `Callable`
"typing_extensions",
}
except AttributeError:
return False
def _hashable(x: object | type) -> TypeGuard[Hashable]:
"""Check if an object is hashable.
Args:
x (object): Object to check.
Returns:
bool: `True` if `x` is hashable and `False` otherwise.
"""
try:
hash(x)
return True
except TypeError:
return False
type_mapping: dict[type, type] = {}
"""dict: When running :func:`resolve_type_hint`, map keys in this dictionary to the
values."""
[docs]
def resolve_type_hint(x: object, /) -> object:
"""Resolve all :class:`ResolvableType` in a type or type hint.
Args:
x (type or type hint): Type hint.
Returns:
type or type hint: `x`, but with all :class:`ResolvableType`\\s resolved.
"""
if _hashable(x) and isinstance(x, type) and x in type_mapping:
return resolve_type_hint(type_mapping[x])
elif _is_hint(x):
origin = get_origin(x)
args = get_args(x)
if args == ():
# `origin` might not make sense here. For example, `get_origin(Any)`
# is `None`. Since the hint wasn't subscripted, the right thing is
# to return the hint itself.
return x
if origin is UnionType: # The new union syntax was used.
return reduce(or_, (resolve_type_hint(arg) for arg in args))
else:
# Do not resolve the arguments for `Literal`s.
if origin is not Literal:
resolved_args = resolve_type_hint(args)
assert isinstance(resolved_args, tuple)
args = resolved_args
# Ensure origin is not `None` before indexing.
assert origin is not None
return origin[args]
elif x is None or x is Ellipsis:
return x
elif isinstance(x, tuple):
return tuple(resolve_type_hint(arg) for arg in x)
elif isinstance(x, list):
return [resolve_type_hint(arg) for arg in x]
elif isinstance(x, type):
if not isinstance(x, ResolvableType):
return x
elif isinstance(x, ModuleType) and not x.retrieve():
# If the type could not be retrieved, then just return the
# wrapper. Namely, `x.resolve()` will then return `x`, which
# means that the below call will result in an infinite
# recursion.
return x
return resolve_type_hint(x.resolve())
elif get_origin(x) is not None:
# Parameterised user-defined Generic, e.g. ``Box[int]`` where ``Box``
# is a subclass of ``Generic[T]``. ``_is_hint`` does not recognise
# these because their ``__module__`` points to user code, but they
# still carry origin/args that we can recurse into.
origin = get_origin(x)
args = get_args(x)
if args:
resolved_args = resolve_type_hint(args)
assert isinstance(resolved_args, tuple)
assert origin is not None
return origin[resolved_args]
return x
# For example, `Is[lambda x: x > 0]` is an example of a `BeartypeValidator`.
# We shouldn't resolve those.
elif isinstance(x, BeartypeValidator):
return x
else:
warnings.warn(
f"Could not resolve the type hint of `{x}`. "
f"I have ended the resolution here to not make your code break, but some "
f"types might not be working correctly. "
f"Please open an issue at https://github.com/beartype/plum.",
stacklevel=2,
)
return x
[docs]
def is_faithful(x: object, /) -> bool:
"""Check whether a type hint is faithful.
A type or type hint `t` is defined _faithful_ if, for all `x`, the following holds
true::
isinstance(x, x) == issubclass(type(x), t)
You can control whether types are faithful or not by setting the attribute
`__faithful__`::
class UnfaithfulType:
__faithful__ = False
Args:
x (type or type hint): Type hint.
Returns:
bool: Whether `x` is faithful or not.
"""
return _is_faithful(resolve_type_hint(x))
UNION_TYPES = (typing.Union, UnionType, typing.Optional)
class _SupportsDunderFaithful(Protocol): # type: ignore[misc]
__faithful__: bool
def _has_dunder_faithful(x: type, /) -> TypeGuard[_SupportsDunderFaithful]:
"""Check whether `x` has the `__faithful__` attribute."""
return hasattr(x, "__faithful__")
def _is_faithful(x: object, /) -> bool:
if _is_hint(x):
origin = get_origin(x)
args = get_args(x)
if args == ():
# Unsubscripted type hints tend to be faithful. For example, `Any`,
# `List`, `Tuple`, `Dict`, `Callable`, and `Generator` are. When we
# come across a counter-example, we will refine this logic.
return True
if origin in UNION_TYPES:
return all(is_faithful(arg) for arg in args)
return False
elif x is None or x == Ellipsis:
return True
elif isinstance(x, (tuple, list)):
return all(is_faithful(arg) for arg in x)
elif get_origin(x) is not None:
# Parameterised user-defined Generic, e.g. ``Box[int]``. These are
# never faithful: ``isinstance(Box("a"), Box)`` is True regardless of
# the type argument, so the element type cannot be inferred from the
# container type alone.
return False
elif isinstance(x, type):
if _has_dunder_faithful(x):
return x.__faithful__
else:
# This is the fallback method. Check whether `__instancecheck__` is default
# or not. If it is, assume that it is faithful.
return type(x).__instancecheck__ in {
type.__instancecheck__,
abc.ABCMeta.__instancecheck__,
}
else:
warnings.warn(
f"Could not determine whether `{x}` is faithful or not. "
f"I have concluded that the type is not faithful, so your code might run "
f"with subpar performance. "
f"Please open an issue at https://github.com/beartype/plum.",
stacklevel=2,
)
return False