Source code for plum._type

__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