#!/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.typing import (
Any,
FrozenSet,
Generic,
Iterable,
Tuple,
overload,
)
from beartype._conf.confcls import (
BEARTYPE_CONF_DEFAULT,
BeartypeConf,
)
from beartype._data.hint.datahinttyping import T
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.
[docs]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 that *cannot* reasonably be compared with one another).
Type hint wrappers are thus usable in algorithms and data structures
requiring at most a partial ordering over their input.
Examples
--------
.. code-block:: pycon
>>> 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', ...]: ...
# Note that the actual implementation of this overload is intentionally:
# * *NOT* decorated by the standard @overload decorator.
# * *NOT* annotated by type hints. By PEP 484, only the signatures of
# @overload-decorated callables are annotated by type hints.
def __getitem__(self, index):
'''
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 }..................
[docs] def die_if_unbearable(
self,
# Mandatory flexible parameters.
obj: object,
# Optional keyword-only parameters.
*,
conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
exception_prefix: str = 'die_if_unbearable() ',
) -> None:
'''
Raise an exception if the passed arbitrary object violates this type
hint under the passed beartype configuration.
To configure the type of violation exception raised by this method, set
the :attr:`.BeartypeConf.violation_door_type` option of the passed
``conf`` parameter accordingly.
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.
exception_prefix : str, optional
Human-readable label prefixing the representation of this object in
the exception message. Defaults to a reasonably sensible string.
Raises
------
``conf.violation_door_type``
If this object violates this hint.
beartype.roar.BeartypeDecorHintNonpepException
If this hint is *not* PEP-compliant (i.e., complies with *no* Python
Enhancement Proposals (PEPs) currently supported by
:mod:`beartype`).
beartype.roar.BeartypeDecorHintPepUnsupportedException
If this hint is currently unsupported by :mod:`beartype`.
Examples
--------
.. code-block:: pycon
>>> 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,
exception_prefix=exception_prefix,
)
[docs] def is_bearable(
self,
# Mandatory flexible parameters.
obj: object,
# Optional keyword-only parameters.
*, conf: BeartypeConf = BEARTYPE_CONF_DEFAULT,
) -> 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
--------
.. code-block:: python
>>> 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!
[docs] @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)