#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Beartype **Decidedly Object-Oriented Runtime-checking (DOOR) superclass**
(i.e., root of the object-oriented type hint class hierarchy encapsulating the
non-object-oriented type hint API standardized by the :mod:`typing` module).

This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ TODO                               }....................
#FIXME: Slot "TypeHint" attributes for lookup efficiency, please.
#FIXME: Privatize most (...or perhaps all) public instance variables, please.

# ....................{ IMPORTS                            }....................
from beartype.door._doorcheck import (
    die_if_unbearable,
    is_bearable,
)
from beartype.door._cls.doormeta import _TypeHintMeta
from beartype.door._doortest import die_unless_typehint
from beartype.door._doortyping import T
from beartype.typing import (
    Any,
    FrozenSet,
    Generic,
    Iterable,
    # Optional,
    Tuple,
    Union,
    overload,
)
from beartype._conf.confcls import (
    BEARTYPE_CONF_DEFAULT,
    BeartypeConf,
)
from beartype._data.hint.datahinttyping import CallableMethodGetitemArg
from beartype._util.cache.utilcachecall import (
    method_cached_arg_by_id,
    property_cached,
)
from beartype._util.hint.pep.utilpepget import (
    get_hint_pep_args,
    get_hint_pep_origin_type_or_none,
    get_hint_pep_sign_or_none,
)
from beartype._util.hint.utilhinttest import is_hint_ignorable
from beartype._util.utilobject import get_object_type_basename

# ....................{ SUPERCLASSES                       }....................
#FIXME: Subclass all applicable "collections.abc" ABCs for explicitness, please.
#FIXME: Document all public and private attributes of this class, please.
class TypeHint(Generic[T], metaclass=_TypeHintMeta):
    '''
    Abstract base class (ABC) of all **type hint wrapper** (i.e., high-level
    object encapsulating a low-level type hint augmented with a magically
    object-oriented Pythonic API, including equality and rich comparison
    testing) subclasses.

    Sorting
    -------
    **Type hint wrappers are partially ordered** with respect to one another.
    Type hints wrappers support all binary comparators (i.e., ``==``, ``!=``,
    ``<``, ``<=``, ``>``, and ``>=``) such that for any three type hint wrappers
    ``a``, ``b`, and ``c``:

    * ``a ≤ a`` (i.e., **reflexivity**).
    * If ``a ≤ b`` and ``b ≤ c``, then ``a ≤ c`` (i.e., **transitivity**).
    * If ``a ≤ b`` and ``b ≤ a``, then ``a == b`` (i.e., **antisymmetry**).

    **Type hint wrappers are not totally ordered,** however. Like unordered
    sets, type hint wrappers do *not* satisfy **totality** (i.e., either ``a ≤
    b`` or ``b ≤ a``, which is *not* necessarily the case for incommensurable
    type hint wrappers).

    Type hint wrappers are thus usable in algorithms and data structures
    requiring at most a partial ordering over their input.

    Examples
    --------
        >>> from beartype.door import TypeHint
        >>> hint_a = TypeHint(Callable[[str], list])
        >>> hint_b = TypeHint(Callable[Union[int, str], Sequence[Any]])
        >>> hint_a <= hint_b
        True
        >>> hint_a > hint_b
        False
        >>> hint_a.is_subhint(hint_b)
        True
        >>> list(hint_b)
        [TypeHint(typing.Union[int, str]), TypeHint(typing.Sequence[typing.Any])]

    Attributes
    ----------
    _args : Tuple[object, ...]
        Tuple of the zero or more low-level child type hints subscripting
        (indexing) the low-level parent type hint wrapped by this wrapper.
    _hint : T
        Low-level type hint wrapped by this wrapper.
    _hint_sign : beartype._data.hint.pep.sign.datapepsigncls.HintSign | None
        Either:

        * If this hint is PEP-compliant and thus uniquely identified by a
          :mod:`beartype`-specific sign, that sign.
        * Else (i.e., if this hint is an isinstanceable class), :data:`None`.
    _origin: type
        Either:

        * If this hint originates from an **isinstanceable class** such that all
          objects satisfying this hint are instances of that class, that class.
        * Else, the root superclass :class:`object` of *all* classes,
          guaranteeing sanity when this instance variable is passed as either
          the first or second parameters to the :func:`issubclass` builtin.
    '''

    # ..................{ INITIALIZERS                       }..................
    def __init__(self, hint: T) -> None:
        '''
        Initialize this type hint wrapper from the passed low-level type hint.

        Parameters
        ----------
        hint : object
            Low-level type hint to be wrapped by this wrapper.
        '''

        # Classify all passed parameters. Note that this type hint is guaranteed
        # to be a type hint by validation performed by this metaclass __init__()
        # method.
        self._hint = hint

        # Sign uniquely identifying this and that hint if any *OR* "None"
        self._hint_sign = get_hint_pep_sign_or_none(hint)

        # Isinstance class originating this hint if any *OR* "None" otherwise,
        # defined as either...
        self._origin: type = (
            # If this hint originates from an origin type, that type;
            get_hint_pep_origin_type_or_none(hint) or
            # Else, this hint does *NOT* originate from an origin type. In this
            # case, the root superclass "object" of *ALL* classes, guaranteeing
            # sanity when this instance variable is passed as either the first
            # or second parameters to the issubclass() builtin.
            object
        )

        # Tuple of all low-level child type hints of this hint.
        self._args = self._make_args()

    # ..................{ DUNDERS                            }..................
    def __hash__(self) -> int:
        '''
        Hash of the low-level immutable type hint wrapped by this immutable
        wrapper.

        Defining this method satisfies the :class:`collections.abc.Hashable`
        abstract base class (ABC), enabling this wrapper to be used as in
        hashable containers (e.g., dictionaries, sets).
        '''

        return hash(self._hint)


    def __repr__(self) -> str:
        '''
        Machine-readable representation of this type hint wrapper.
        '''

        # Unqualified name of the concrete subclass wrapping this hint.
        hint_wrapper_basename = get_object_type_basename(self)

        # If this concrete subclass is currently private, deviously hide this
        # implementation detail by defaulting to the unqualified name of this
        # public "TypeHint" superclass instead.
        if hint_wrapper_basename[0] == '_':
            hint_wrapper_basename = 'TypeHint'
        # Else, this concrete subclass is public.

        # Return this machine-readable representation.
        return f'{hint_wrapper_basename}({repr(self._hint)})'

    # ..................{ DUNDERS ~ compare : equals         }..................
    # Note that we intentionally avoid typing this method as returning
    # "Union[bool, NotImplementedType]". Why? Because mypy in particular has
    # epileptic fits about "NotImplementedType". This is *NOT* worth the agony!
    @method_cached_arg_by_id
    def __eq__(self, other: object) -> bool:
        '''
        ``True`` only if the low-level type hint wrapped by this wrapper is
        semantically equivalent to the other low-level type hint wrapped by the
        passed wrapper.

        This tester is memoized for efficiency, as Python implicitly calls this
        dunder method on hashable-based container lookups (e.g.,
        :meth:`dict.get`) expected to be ``O(1)`` fast.

        Parameters
        ----------
        other : object
            Other type hint to be tested against this type hint.

        Returns
        -------
        bool
            ``True`` only if this type hint is equal to that other hint.
        '''

        # If that object is *NOT* a type hint wrapper, defer to either:
        # * If the class of that object defines a similar __eq__() method
        #   supporting the "TypeHint" API, that method.
        # * Else, Python's builtin C-based fallback equality comparator that
        #   merely compares whether two objects are identical (i.e., share the
        #   same object ID).
        if not isinstance(other, TypeHint):
            return NotImplemented
        # Else, that object is also a type hint wrapper.

        # Defer to the subclass-specific implementation of this test.
        return self._is_equal(other)


    def __ne__(self, other: object) -> bool:
        return not (self == other)

    # ..................{ DUNDERS ~ compare : rich           }..................
    def __le__(self, other: object) -> bool:
        '''
        :data:`True` if this hint is a subhint of the passed hint.
        '''

        if not isinstance(other, TypeHint):
            return NotImplemented

        return self.is_subhint(other)


    def __lt__(self, other: object) -> bool:
        '''
        :data:`True` if this hint is a strict subhint of the passed hint.
        '''

        if not isinstance(other, TypeHint):
            return NotImplemented

        return self.is_subhint(other) and self != other


    def __ge__(self, other: object) -> bool:
        '''
        :data:`True` if this hint is a superhint of the passed hint.
        '''

        if not isinstance(other, TypeHint):
            return NotImplemented

        return self.is_superhint(other)


    def __gt__(self, other: object) -> bool:
        '''
        :data:`True` if this hint is a strict superhint of the passed hint.
        '''

        if not isinstance(other, TypeHint):
            return NotImplemented

        return self.is_superhint(other) and self != other

    # ..................{ DUNDERS ~ iterable                 }..................
    def __contains__(self, hint_child: 'TypeHint') -> bool:
        '''
        :data:`True` only if the low-level type hint wrapped by the passed
        **type hint wrapper** (i.e., :class:`TypeHint` instance) is a child type
        hint originally subscripting the low-level parent type hint wrapped by
        this :class:`TypeHint` instance.
        '''

        # Sgt. Pepper's One-liners GitHub Club Band.
        return hint_child in self._args_wrapped_frozenset


    def __iter__(self) -> Iterable['TypeHint']:
        '''
        Generator iteratively yielding all **children type hint wrappers**
        (i.e., :class:`TypeHint` instances wrapping all low-level child type
        hints originally subscripting the low-level parent type hint wrapped by
        this :class:`TypeHint` instance).

        Defining this method satisfies the :class:`collections.abc.Iterable`
        abstract base class (ABC).
        '''

        # For those who are about to one-liner, we salute you.
        yield from self._args_wrapped_tuple

    # ..................{ DUNDERS ~ iterable : item          }..................
    # Inform static type-checkers of the one-to-one correspondence between the
    # type of the object subscripting an instance of this class with the type of
    # the object returned by that subscription. Note this constraint is strongly
    # inspired by this erudite StackOverflow answer:
    #     https://stackoverflow.com/a/71183076/2809027

    @overload
    def __getitem__(self, index: int) -> 'TypeHint': ...
    @overload
    def __getitem__(self, index: slice) -> Tuple['TypeHint', ...]: ...

    def __getitem__(self, index: CallableMethodGetitemArg) -> (
        Union['TypeHint', Tuple['TypeHint', ...]]):
        '''
        Either:

        * If the passed object is an integer, then this type hint wrapper was
          subscripted by either a positive 0-based absolute index or a negative
          -1-based relative index. In either case, this dunder method returns
          the **child type hint wrapper** (i.e., :class:`TypeHint` instance
          wrapping a low-level child type hint originally subscripting the
          low-level parent type hint wrapped by this :class:`TypeHint` instance)
          with the same index.
        * If the passed object is a slice, then this type hint wrapper was
          subscripted by a range of such indices. In this case, this dunder
          method returns a tuple of the zero or more child type hint wrappers
          with the same indices.

        Parameters
        ----------
        index : Union[int, slice]
            Either:

            * Positive 0-based absolute index or negative -1-based relative
              index of the child type hint originally subscripting the parent
              type hint wrapped by this :class:`TypeHint` instance to be
              returned wrapped by a new :class:`TypeHint` instance.
            * Slice of such indices of the zero or more child type hints
              originally subscripting the parent type hint wrapped by this
              :class:`TypeHint` instance to be returned in a tuple of these
              child type hints wrapped by new :class:`TypeHint` instances.

        Returns
        -------
        Union['TypeHint', Tuple['TypeHint', ...]]
            Child type hint wrapper(s) at these ind(ex|ices), as detailed above.
        '''

        # Defer validation of the correctness of the passed index or slice to
        # the low-level tuple.__getitem__() dunder method. Though we could (and
        # possibly should) perform that validation here, doing so is non-trivial
        # in the case of both a negative relative index *AND* a passed slice.
        # This trivial approach suffices for now.
        return self._args_wrapped_tuple[index]

    # ..................{ DUNDERS ~ iterable : sized         }..................
    #FIXME: Unit test us up, please.
    def __bool__(self) -> bool:
        '''
        :data:`True` only if the low-level parent type hint wrapped by this
        wrapper was subscripted by at least one child type hint.
        '''

        # See __len__() for further commentary.
        return bool(self._args_wrapped_tuple)


    #FIXME: Unit test us up, please.
    def __len__(self) -> int:
        '''
        Number of low-level child type hints subscripting the low-level parent
        type hint wrapped by this wrapper.

        Defining this method satisfies the :class:`collections.abc.Sized`
        abstract base class (ABC).
        '''

        # Return the exact length of the same iterable returned by the
        # __iter__() dunder method rather than the possibly differing length of
        # the "self._args" tuple, for safety. Theoretically, these two iterables
        # should exactly coincide in length. Pragmatically, it's best to assume
        # nothing in the murky waters we swim in.
        return len(self._args_wrapped_tuple)

    # ..................{ PROPERTIES ~ read-only             }..................
    # Read-only properties intentionally defining *NO* corresponding setter.

    #FIXME: Unit test us up, please.
    @property
    def args(self) -> tuple:
        '''
        Tuple of the zero or more low-level child type hints subscripting
        (indexing) the low-level parent type hint wrapped by this wrapper.
        '''

        # Who could argue with a working one-liner? Not you. Surely, not you.
        return self._args


    @property
    def hint(self) -> T:
        '''
        **Original type hint** (i.e., low-level PEP-compliant type hint wrapped
        by this wrapper at :meth:`TypeHint.__init__` instantiation time).
        '''

        # Q: Can one-liners solve all possible problems? A: Yes.
        return self._hint


    @property
    def is_ignorable(self) -> bool:
        '''
        :data:`True` only if this type hint is **ignorable** (i.e., conveys
        *no* meaningful semantics despite superficially appearing to do so).

        While one might expect the set of all ignorable type hints to be both
        finite and small, this set is actually **countably infinite** in size.
        Countably infinitely many type hints are ignorable. This includes:

        * :attr:`typing.Any`, by design.
        * :class:`object`, the root superclass of all types. Ergo, parameters
          and return values annotated as :class:`object` unconditionally match
          *all* objects under :func:`isinstance`-based type covariance and thus
          semantically reduce to unannotated parameters and return values.
        * The unsubscripted :attr:`typing.Optional` singleton, which
          semantically expands to the implicit ``Optional[Any]`` type hint under
          :pep:`484`. Since :pep:`484` also stipulates that all ``Optional[t]``
          type hints semantically expand to ``Union[t, type(None)]`` type hints
          for arbitrary arguments ``t``, ``Optional[Any]`` semantically expands
          to merely ``Union[Any, type(None)]``. Since all unions subscripted by
          :attr:`typing.Any` semantically reduce to merely :attr:`typing.Any`,
          the unsubscripted :attr:`typing.Optional` singleton also reduces to
          merely :attr:`typing.Any`. This intentionally excludes the
          ``Optional[type(None)]`` type hint, which the :mod:`typing` module
          reduces to merely ``type(None)``.
        * The unsubscripted :attr:`typing.Union` singleton, which
          semantically reduces to :attr:`typing.Any` by the same argument.
        * Any subscription of :attr:`typing.Union` by one or more ignorable type
          hints. There exists a countably infinite number of such subscriptions,
          many of which are non-trivial to find by manual inspection. The
          ignorability of a union is a transitive property propagated "virally"
          from child to parent type hints. Consider:

          * ``Union[Any, bool, str]``. Since :attr:`typing.Any` is ignorable,
            this hint is trivially ignorable by manual inspection.
          * ``Union[str, List[int], NewType('MetaType', Annotated[object,
            53])]``. Although several child type hints of this union are
            non-ignorable, the deeply nested :class:`object` child type hint is
            ignorable by the argument above. It transitively follows that the
            ``Annotated[object, 53]`` parent type hint subscripted by
            :class:`object`, the :obj:`typing.NewType` parent type hint aliased
            to ``Annotated[object, 53]``, *and* the entire union subscripted by
            that :obj:`typing.NewType` are themselves all ignorable as well.

        * Any subscription of :attr:`typing.Annotated` by one or more ignorable
          type hints. As with :attr:`typing.Union`, there exists a countably
          infinite number of such subscriptions. (See the prior item.)
        * The :class:`typing.Generic` and :class:`typing.Protocol` superclasses,
          both of which impose no constraints *in and of themselves.* Since all
          possible objects satisfy both superclasses. Both superclasses are
          synonymous to the ignorable :class:`object` root superclass: e.g.,

          .. code-block:: pycon

             >>> from typing as Protocol
             >>> isinstance(object(), Protocol)
             True
             >>> isinstance('wtfbro', Protocol)
             True
             >>> isinstance(0x696969, Protocol)
             True

        * Any subscription of either the :class:`typing.Generic` or
          :class:`typing.Protocol` superclasses, regardless of whether the child
          type hints subscripting those superclasses are ignorable or not.
          Subscripting a type that conveys no meaningful semantics continues to
          convey no meaningful semantics. For example, the type hints
          ``typing.Generic[typing.Any]`` and ``typing.Generic[str]`` are both
          equally ignorable – despite the :class:`str` class being otherwise
          unignorable in most type hinting contexts.
        * And frankly many more. And... *now we know why this tester exists.*

        This property is memoized for efficiency.

        Returns
        -------
        bool
            :data:`True` only if this type hint is ignorable.
        '''

        # Mechanic: Somebody set up us the bomb.
        return is_hint_ignorable(self._hint)

    # ..................{ CHECKERS                           }..................
    def die_if_unbearable(
        self,

        # Mandatory flexible parameters.
        obj: object,

        # Optional keyword-only parameters.
        *,
        conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
    ) -> None:
        '''
        Raise an exception if the passed arbitrary object violates this type
        hint under the passed beartype configuration.

        Parameters
        ----------
        obj : object
            Arbitrary object to be tested against this hint.
        conf : BeartypeConf, optional
            **Beartype configuration** (i.e., self-caching dataclass
            encapsulating all settings configuring type-checking for the passed
            object). Defaults to ``BeartypeConf()``, the default ``O(1)``
            constant-time configuration.

        Raises
        ------
        BeartypeDoorHintViolation
            If this object violates this hint.

        Examples
        --------
            >>> from beartype.door import TypeHint
            >>> TypeHint(list[str]).die_if_unbearable(
            ...     ['And', 'what', 'rough', 'beast,'], )
            >>> TypeHint(list[str]).die_if_unbearable(
            ...     ['its', 'hour', 'come', 'round'], list[int])
            beartype.roar.BeartypeDoorHintViolation: Object ['its', 'hour',
            'come', 'round'] violates type hint list[int], as list index 0 item
            'its' not instance of int.
        '''

        # One-liner, one love, one heart. Let's get together and code alright.
        die_if_unbearable(obj=obj, hint=self._hint, conf=conf)


    #FIXME: Submit an upstream mypy issue. Since mypy correctly accepts our
    #comparable beartype.door.is_bearable() function, mypy should also
    #accept this equivalent method. mypy currently fails to do so with this
    #false positive:
    #    error: Variable "beartype.door._cls.doorsuper.TypeHint.TypeGuard" is not
    #    valid as a type
    #
    #Clearly, mypy fails to percolate the type variable "T" from our
    #pseudo-superclass "Generic[T]" onto this return annotation. *sigh*
    def is_bearable(
        self,

        # Mandatory flexible parameters.
        obj: object,

        # Optional keyword-only parameters.
        *, conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
    #FIXME: Re-enable after static type-checkers support "TypeGuard[T]". See
    #"_doorcheck" for further details, please.
    # ) -> TypeGuard[T]:
    ) -> bool:
        '''
        :data:`True` only if the passed arbitrary object satisfies this type
        hint under the passed beartype configuration.

        Parameters
        ----------
        obj : object
            Arbitrary object to be tested against this hint.
        conf : BeartypeConf, optional
            **Beartype configuration** (i.e., self-caching dataclass
            encapsulating all settings configuring type-checking for the passed
            object). Defaults to ``BeartypeConf()``, the default
            constant-time configuration.

        Returns
        -------
        bool
            :data:`True` only if this object satisfies this hint.

        Raises
        ------
        beartype.roar.BeartypeDecorHintForwardRefException
            If this hint contains one or more relative forward references, which
            this tester explicitly prohibits to improve both the efficiency and
            portability of calls to this tester.

        Examples
        --------
            >>> from beartype.door import TypeHint
            >>> TypeHint(list[str]).is_bearable(['Things', 'fall', 'apart;'])
            True
            >>> TypeHint(list[int]).is_bearable(
            ...     ['the', 'centre', 'cannot', 'hold;'])
            False
        '''

        # One-liners justify their own existence.
        return is_bearable(obj=obj, hint=self._hint, conf=conf)

    # ..................{ TESTERS ~ subhint                  }..................
    # Note that the @method_cached_arg_by_id rather than @callable_cached
    # decorator is *ABSOLUTELY* required here. Why? Because the @callable_cached
    # decorator internally caches the passed "other" argument as the key of a
    # dictionary. Subsequent calls to this method when passed the same argument
    # lookup that "other" in that dictionary. Since dictionary lookups
    # implicitly call other.__eq__() to resolve key collisions *AND* since the
    # TypeHint.__eq__() method calls TypeHint.is_subhint(), infinite recursion!
    @method_cached_arg_by_id
    def is_subhint(self, other: 'TypeHint') -> bool:
        '''
        :data:`True` only if this type hint is a **subhint** of the passed type
        hint.

        This tester method is memoized for efficiency.

        Parameters
        ----------
        other : TypeHint
            Other type hint to be tested against this type hint.

        Returns
        -------
        bool
            :data:`True` only if this type hint is a subhint of that other hint.

        See Also
        --------
        :func:`beartype.door.is_subhint`
            Further details.
        '''

        # If the passed object is *NOT* a type hint wrapper, raise an exception.
        die_unless_typehint(other)
        # Else, that object is a type hint wrapper.

        # Return true only if either...
        return (
            # That other hint is the "typing.Any" catch-all. By definition, that
            # hint is the superhint of *ALL* hints -- including "typing.Any"
            # itself. Ergo, this hint is necessarily a subhint of that hint.
            other._hint is Any or
            # Else, that other hint is *NOT* the "typing.Any" catch-all. In this
            # case, defer to the subclass-specific implementation of this test.
            self._is_subhint(other)
        )


    def is_superhint(self, other: 'TypeHint') -> bool:
        '''
        :data:`True` only if this type hint is a **superhint** of the passed
        type hint.

        This tester method is memoized for efficiency.

        Parameters
        ----------
        other : TypeHint
            Other type hint to be tested against this type hint.

        Returns
        -------
        bool
            :data:`True` only if this type hint is a superhint of that other
            hint.

        See Also
        --------
        :func:`beartype.door.is_subhint`
            Further details.
        '''

        # If the passed object is *NOT* a type hint wrapper, raise an exception.
        die_unless_typehint(other)
        # Else, that object is a type hint wrapper.

        # Return true only if this hint is a superhint of the passed hint.
        return other.is_subhint(self)

    # ..................{ PRIVATE                            }..................
    # Subclasses are encouraged to override these concrete methods defaulting to
    # general-purpose implementations suitable for most subclasses.

    # ..................{ PRIVATE ~ factories                }..................
    def _make_args(self) -> tuple:
        '''
        Tuple of the zero or more low-level child type hints subscripting
        (indexing) the low-level parent type hint wrapped by this wrapper, which
        the :meth:`TypeHint.__init__` method assigns to the :attr:`_args`
        instance variable of this wrapper.

        Subclasses are advised to override this method to set the :attr:`_args`
        instance variable of this wrapper in a subclass-specific manner.
        '''

        # We are the one-liner. We are the codebase.
        return get_hint_pep_args(self._hint)

    # ..................{ PRIVATE ~ testers                  }..................
    def _is_equal(self, other: 'TypeHint') -> bool:
        '''
        :data:`True` only if the low-level type hint wrapped by this wrapper is
        semantically equivalent to the other low-level type hint wrapped by the
        passed wrapper.

        Subclasses are advised to override this method to implement the public
        :meth:`is_subhint` tester method (which internally defers to this
        private tester method) in a subclass-specific manner. Since the default
        implementation is guaranteed to suffice for *all* possible use cases,
        subclasses should override this method only for efficiency reasons; the
        default implementation calls the :meth:`is_subhint` method twice and is
        thus *not* necessarily the optimal implementation for subclasses.
        Notably, the default implementation exploits the well-known syllogism
        between two partially ordered items ``A`` and ``B``:

        * If ``A <= B`` and ``A >= B``, then ``A == B``.

        This private tester method is *not* memoized for efficiency, as the
        caller is guaranteed to be the public :meth:`__eq__` tester method,
        which is already memoized.

        Parameters
        ----------
        other : TypeHint
            Other type hint to be tested against this type hint.

        Returns
        -------
        bool
            :data:`True` only if this type hint is equal to that other hint.
        '''

        # Return true only if both...
        #
        # Note that this conditional implements the trivial boolean syllogism
        # that we all know and adore: "If A <= B and B <= A, then A == B".
        return (
            # This union is a subhint of that object.
            self.is_subhint(other) and
            # That object is a subhint of this union.
            other.is_subhint(self)
        )

    # ..................{ PRIVATE ~ testers : subhint        }..................
    def _is_subhint(self, other: 'TypeHint') -> bool:
        '''
        :data:`True` only if this type hint is a **subhint** of the passed type
        hint.

        Subclasses are advised to override this method to implement the public
        :meth:`is_subhint` tester method (which internally defers to this
        private tester method) in a subclass-specific manner.

        This private tester method is *not* memoized for efficiency, as the
        caller is guaranteed to be the public :meth:`is_subhint` tester method,
        which is already memoized.

        Parameters
        ----------
        other : TypeHint
            Other type hint to be tested against this type hint.

        Returns
        -------
        bool
            :data:`True` only if this type hint is a subhint of that other hint.

        See Also
        --------
        :func:`beartype.door.is_subhint`
            Further details.
        '''

        # Return true only if this hint is a subhint of *ANY* branch of that
        # other hint.
        return any(
            self._is_subhint_branch(other_branch)
            for other_branch in other._branches
        )


    def _is_subhint_branch(self, branch: 'TypeHint') -> bool:
        '''
        :data:`True` only if this type hint is a subhint of the passed branch of
        another type hint passed to a parent call of the :meth:`is_subhint`
        method, itself called by the :meth:`__le__` dunder method.

        Parameters
        ----------
        branch : TypeHint
            Conditional branch of another type hint to be tested against.

        See Also
        --------
        :meth:`__le__`
            Further details.
        '''

        # If that branch is unsubscripted, assume that branch to have been
        # subscripted by "Any" and simply check for compatible origin types.
        if branch._is_args_ignorable:
            # print(f'is_subhint_branch({self}, {branch} [unsubscripted])')
            return issubclass(self._origin, branch._origin)
        # Else, that branch is subscripted.

        # Return true only if...
        return (
            # That branch is also a type hint wrapper of the same concrete
            # subclass as this type hint wrapper *AND*...
            isinstance(branch, type(self)) and
            # The class originating this hint is a subclass of the class
            # originating that branch...
            issubclass(self._origin, branch._origin) and
            # All child type hints of this parent type hint are subhints of the
            # corresponding child type hints of that branch.
            all(
                self_child <= branch_child
                for self_child, branch_child in zip(
                    self._args_wrapped_tuple, branch._args_wrapped_tuple)
            )
        )

    # ..................{ PRIVATE ~ properties : read-only   }..................
    # Read-only properties intentionally defining *NO* corresponding setter.

    @property  # type: ignore
    @property_cached
    def _args_wrapped_tuple(self) -> Tuple['TypeHint', ...]:
        '''
        Tuple of the zero or more high-level **child type hint wrappers** (i.e.,
        :class:`TypeHint` instances) wrapping the low-level child type hints
        subscripting (indexing) the low-level parent type hint wrapped by this
        wrapper.

        This attribute is intentionally defined as a memoized property to
        minimize space and time consumption for use cases *not* accessing this
        attribute.
        '''

        # One-liner, don't fail us now!
        return tuple(TypeHint(hint_child) for hint_child in self._args)


    @property  # type: ignore
    @property_cached
    def _args_wrapped_frozenset(self) -> FrozenSet['TypeHint']:
        '''
        Frozen set of the zero or more high-level child **type hint wrappers**
        (i.e., :class:`TypeHint` instances) wrapping the low-level child type
        hints subscripting (indexing) the low-level parent type hint wrapped by
        this wrapper.

        This attribute is intentionally defined as a memoized property to
        minimize space and time consumption for use cases *not* accessing this
        attribute.
        '''

        return frozenset(self._args_wrapped_tuple)


    @property  # type: ignore
    @property_cached
    def _branches(self) -> Iterable['TypeHint']:
        '''
        Immutable iterable of all **branches** (i.e., high-level type hint
        wrappers encapsulating all low-level child type hints subscripting
        (indexing) the low-level parent type hint encapsulated by this
        high-level parent type hint wrappers if this is a union (and thus an
        instance of the :class:`UnionTypeHint` subclass) *or* the 1-tuple
        containing only this instance itself otherwise) of this type hint
        wrapper.

        This property enables the child type hints of both :pep:`484`- and
        :pep:`604`-compliant unions (e.g., :attr:`typing.Union`,
        :attr:`typing.Optional`, and ``|``-delimited type objects) to be handled
        transparently *without* special cases in subclass implementations.
        '''

        # Default to returning the 1-tuple containing only this instance, as
        # *ALL* subclasses except "_HintTypeUnion" require this default.
        return (self,)


    @property  # type: ignore
    @property_cached
    def _is_args_ignorable(self) -> bool:
        '''
        :data:`True` only if this hint is effectively **unsubscripted** (i.e.,
        either indexed by *no* child type hints or only indexed by ignorable
        child type hints).

        If :data:`True`, this hint can be trivially and efficiently evaluated
        by simply inspecting its :attr:`_origin` property. Relevant type hints
        include:

        * Unsubscripted type hint factories (e.g., ``Tuple``, ``Callable``).
        * Type hints subscripted only by ignorable child type hints (e.g.,
          ``Tuple[Any, ...]``, ``Callable[..., Any]``).

        This boolean trivializes comparisons between syntactically unrelated
        type hints that are nonetheless semantically equivalent: e.g.,

        .. code-block:: pycon

           >>> from beartype.door import TypeHint
           >>> from typing import Any, Tuple

           # These type hints are all semantically equivalent despite being
           # mostly syntactically unrelated.
           >>> TypeHint(tuple) == TypeHint(typing.Tuple) == \
           ... TypeHint(typing.Tuple[Any, ...])
           True

        Note that this property is *not* equivalent to the :meth:`is_ignorable`
        property. Although related, a non-ignorable parent type hint can
        trivially have ignorable child type hints (e.g., ``list[Any]``).
        '''

        # Return true only if all child type hints subscripting this parent type
        # hint are themselves ignorable.
        # print(f'[_is_args_ignorable] {self}._args_wrapped_tuple: {self._args_wrapped_tuple}')
        return all(
            hint_child.is_ignorable for hint_child in self._args_wrapped_tuple)
