Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
beartype / _util / hint / utilhinttest.py
Size: Mime:
#!/usr/bin/env python3
# --------------------( LICENSE                            )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **PEP-agnostic type hint tester utilities** (i.e., callables
validating arbitrary objects to be type hints supported by :mod:`beartype`,
regardless of whether those hints comply with PEP standards or not).

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

# ....................{ IMPORTS                            }....................
from beartype._data.hint.pep.datapeprepr import HINTS_REPR_IGNORABLE_SHALLOW
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.hint.nonpep.utilnonpeptest import (
    die_unless_hint_nonpep,
    is_hint_nonpep,
)
from beartype._util.hint.pep.utilpeptest import (
    die_if_hint_pep_unsupported,
    is_hint_pep,
    is_hint_pep_supported,
)

# ....................{ RAISERS                            }....................
def die_unless_hint(
    # Mandatory parameters.
    hint: object,

    # Optional parameters.
    exception_prefix: str = '',
) -> None:
    '''
    Raise an exception unless the passed object is a **supported type hint**
    (i.e., object supported by the :func:`beartype.beartype` decorator as a
    valid type hint annotating callable parameters and return values).

    Specifically, this function raises an exception if this object is neither:

    * A **supported PEP-compliant type hint** (i.e., :mod:`beartype`-agnostic
      annotation compliant with annotation-centric PEPs currently supported
      by the :func:`beartype.beartype` decorator).
    * A **PEP-noncompliant type hint** (i.e., :mod:`beartype`-specific
      annotation intentionally *not* compliant with annotation-centric PEPs).

    Efficiency
    ----------
    This validator is effectively (but technically *not*) memoized. The passed
    ``exception_prefix`` parameter is usually unique to each call to this
    validator; memoizing this validator would uselessly consume excess space
    *without* improving time efficiency. Instead, this validator first calls the
    memoized :func:`is_hint_pep` tester. If that tester returns ``True``, this
    validator immediately returns ``True`` and is thus effectively memoized;
    else, this validator inefficiently raises a human-readable exception without
    memoization. Since efficiency is mostly irrelevant in exception handling,
    this validator remains effectively memoized.

    Parameters
    ----------
    hint : object
        Object to be validated.
    exception_prefix : str, optional
        Human-readable label prefixing the representation of this object in the
        exception message. Defaults to the empty string.

    Raises
    ------
    BeartypeDecorHintPepUnsupportedException
        If this object is a PEP-compliant type hint currently unsupported by
        the :func:`beartype.beartype` decorator.
    BeartypeDecorHintNonpepException
        If this object is neither a:

        * Supported PEP-compliant type hint.
        * Supported PEP-noncompliant type hint.
    '''

    # If this object is a supported type hint, reduce to a noop.
    if is_hint(hint):
        return
    # Else, this object is *NOT* a supported type hint. In this case,
    # subsequent logic raises an exception specific to the passed parameters.

    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # BEGIN: Synchronize changes here with is_hint() below.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    # If this hint is PEP-compliant *AND* currently unsupported by @beartype,
    # raise an exception.
    if is_hint_pep(hint):
        die_if_hint_pep_unsupported(
            hint=hint, exception_prefix=exception_prefix)
    # Else, this hint is *NOT* PEP-compliant. In this case...

    # If this PEP-noncompliant hint but still currently unsupported by
    # @beartype, raise an exception.
    die_unless_hint_nonpep(hint=hint, exception_prefix=exception_prefix)

# ....................{ TESTERS                            }....................
@callable_cached
def is_hint(hint: object) -> bool:
    '''
    :data:`True` only if the passed object is a **supported type hint** (i.e.,
    object supported by the :func:`beartype.beartype` decorator as a valid type
    hint annotating callable parameters and return values).

    This tester is memoized for efficiency.

    Parameters
    ----------
    hint : object
        Object to be validated.

    Returns
    -------
    bool
        :data:`True` only if this object is either:

        * A **PEP-compliant type hint** (i.e., :mod:`beartype`-agnostic
          annotation compliant with annotation-centric PEPs).
        * A **PEP-noncompliant type hint** (i.e., :mod:`beartype`-specific
          annotation intentionally *not* compliant with annotation-centric
          PEPs).

    Raises
    ------
    TypeError
        If this object is **unhashable** (i.e., *not* hashable by the builtin
        :func:`hash` function and thus unusable in hash-based containers like
        dictionaries and sets). All supported type hints are hashable.
    '''

    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # BEGIN: Synchronize changes here with die_unless_hint() above.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    # Return true only if...
    return (
        # This is a PEP-compliant type hint supported by @beartype *OR*...
        is_hint_pep_supported(hint) if is_hint_pep(hint) else
        # This is a PEP-noncompliant type hint, which by definition is
        # necessarily supported by @beartype.
        is_hint_nonpep(hint=hint, is_str_valid=True)
    )


@callable_cached
def is_hint_ignorable(hint: object) -> bool:
    '''
    :data:`True` only if the passed type hint is **ignorable** (i.e., conveys
    *no* meaningful semantics despite superficially appearing to do so).

    This tester is memoized for efficiency.

    Parameters
    ----------
    hint : object
        Type hint to be inspected.

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

    # Avoid circular import dependencies.
    from beartype._util.hint.utilhintget import get_hint_repr

    # Machine-readable representation of this hint.
    hint_repr = get_hint_repr(hint)

    # If this hint is shallowly ignorable, return true.
    if hint_repr in HINTS_REPR_IGNORABLE_SHALLOW:
        return True
    # Else, this hint is *NOT* shallowly ignorable.
    #
    # If this hint is PEP-compliant...
    elif is_hint_pep(hint):
        # Avoid circular import dependencies.
        from beartype._util.hint.pep.utilpeptest import (
            is_hint_pep_ignorable)

        # Defer to the function testing whether this hint is an ignorable
        # PEP-compliant type hint.
        return is_hint_pep_ignorable(hint)
    # Else, this hint is PEP-noncompliant and thus *NOT* deeply ignorable.

    # Since this hint is also *NOT* shallowly ignorable, this hint is
    # unignorable. In this case, return false.
    return False


#FIXME: Unit test us up, please.
def is_hint_uncached(hint: object) -> bool:
    '''
    :data:`True` only if the passed type hint is **uncached** (i.e., hint *not*
    already internally cached by its parent class or module).

    Caveats
    -------
    This function *cannot* be meaningfully memoized, since the passed type hint
    is *not* guaranteed to be cached somewhere. Only functions passed cached
    type hints can be meaningfully memoized. Since this high-level function
    internally defers to unmemoized low-level functions that are ``O(n)`` for
    ``n`` the size of the inheritance hierarchy of this hint, this function
    should be called sparingly.

    Parameters
    ----------
    hint : object
        Type hint to be inspected.

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

    See Also
    ----------
    :func:`beartype._check.convert.convcoerce.coerce_hint_any`
        Further details.
    '''

    # Avoid circular import dependencies.
    from beartype._util.hint.pep.proposal.utilpep585 import (
        is_hint_pep585_builtin_subscripted)
    from beartype._util.hint.pep.proposal.utilpep604 import is_hint_pep604

    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    # CAUTION: Avoid detecting the kind of this hint by calling any of the
    # get_hint_pep_sign_*() family of memoized getters. Doing so would consume
    # excess time and space when this hint is uncached, as passing this hint to
    # any of those getters would then cache against a hint that it is
    # functionally useless to cache against.
    #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    # Return true only if this hint is either...
    return (
        # PEP 585-compliant (e.g., "list[str]"), this hint is *NOT* self-caching
        # (e.g., "list[str] is not list[str]").
        #
        # Note that this additionally includes all third-party type hints that
        # derive from the "types.GenericAlias" superclass, including:
        # * "numpy.typing.NDArray[...]" type hints.
        is_hint_pep585_builtin_subscripted(hint) or
        # PEP 604-compliant (e.g., "int | str"), this hint is *NOT* self-caching
        # (e.g., "int | str is not int | str").
        #
        # Note that this hint could also be implicitly cached by coercing this
        # non-self-caching PEP 604-compliant union into a self-caching PEP
        # 484-compliant union (e.g., from "int | str" to "Union[int, str]").
        # Since doing so would consume substantially more time for *NO* tangible
        # gain, we strongly prefer the current trivial and efficient approach.
        is_hint_pep604(hint)
    )

# ....................{ TESTERS ~ needs                    }....................
@callable_cached
def is_hint_needs_cls_stack(hint: object) -> bool:
    '''
    :data:`True` only if the passed type hint is **type stack-dependent** (i.e.,
    if :mod:`beartype` requires the tuple of all classes lexically declaring the
    class variables or methods annotated by this hint to generate code
    type-checking this hint).

    This tester returns :data:`False` for most hints; only a small subset of
    hints are type stack-dependent. This includes:

    * :pep:`673`-compliant self type hint (i.e., :obj:`typing.Self`), which is
      contextually valid *only* inside a lexical class declaration.

    This tester is memoized for efficiency.

    Motivation
    ----------
    **This tester should only be called to decide whether memoized callables
    should be passed a type stack.** Passing memoized callables a type stack
    substantially reduces the likelihood of a cache hit and thus the
    average-case efficiency of calls to those callables. Notably:

    * Most type hints do *not* require a type stack.
    * Most type hints annotate class variables and methods of differing classes
      and thus do *not* share the same type stack.

    Ergo, passing memoized callables both a type hint *and* a type stack
    effectively unmemoizes those callables. Thankfully, callers can elide this
    inefficiency by calling this tester first; when this tester returns:

    * :data:`False`, the caller can safely pass memoized callables a type hint
      and :data:`None` for the type stack, thus preserving memoization. This is
      the common case and thus absolutely worth optimizing for.
    * :data:`True`, the caller has *no* choice but to pass memoized callables
      both a type hint *and* a type stack. Grr!

    Parameters
    ----------
    hint : object
        Type hint to be inspected.

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

    # Avoid circular import dependencies.
    from beartype._util.hint.utilhintget import get_hint_repr

    # Machine-readable representation of this hint.
    hint_repr = get_hint_repr(hint)

    # Return true only if this representation embeds the representation of
    # either:
    # * A PEP 673-compliant self type hint (i.e., "typing.Self").
    #
    # Note:
    # * The fully-qualified names of exact typing modules (e.g., "typing",
    #   "typing_extensions") is intentionally ignored. Although we could
    #   certainly explicitly test for both, doing so would only needlessly
    #   reduce efficiency. By omitting explicit tests for both, this tester
    #   intentionally returns false positives for extremely edge case hints
    #   whose representations contain syntactically related but semantically
    #   unrelated substrings (e.g., "typing.Literal['.Self']", which is clearly
    #   *NOT* a PEP 673-compliant self type hint but erroneously matched as one
    #   by this heuristic). This is non-ideal but thankfully ignorable. See the
    #   "Motivation" section of the docstring for further commentary.
    # * This string is intentionally *NOT* preceded by a "." delimiter (e.g., as
    #   ".Self" rather than "Self"). Previously, this string was intentionally
    #   preceded by a "." delimiter; doing so satisfied most edge cases while
    #   reducing the likelihood of a false positive. Sadly, doing so also failed
    #   to match "Self" type hints stringified by PEP 563. Grrr!
    # * That the "in" operator is known to be the fastest means of performing
    #   substring matching in Python. Indeed:
    #   * "in" is faster than the str.find() method *SUBSTANTIALLY* faster than
    #     the re.match() function, which is an entire of magnitude slower than
    #     "in".
    #   * re.match() only begins to catch up to "in" when concurrently testing
    #     for more than 10 or so substrings (e.g., r'(0|1|2|3|4|5|6|7|8|9)').
    #
    # See also the extensive timings documented at this StackOverflow question:
    #     https://stackoverflow.com/questions/4901523/whats-a-faster-operation-re-match-search-or-str-find
    return 'Self' in hint_repr