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

'''
**Beartype type-checking testers** (i.e., functions type-checking arbitrary
objects against PEP-compliant type hints, callable at *any* arbitrary time
during the lifecycle of the active Python process).
'''

# ....................{ TODO                              }....................
#FIXME: [CRITICAL DEFECT] The functionality below imports @beartype.beartype,
#which under "-O" reduces to a noop. Woops. Instead, the functionality below
#should import our private top-level @beartype._decor.main.whatevah decorator
#and internally depend upon that instead. *sweat pours down forehead*

#FIXME: Optimize us up, please. See this discussion for voluminous details:
#    https://github.com/beartype/beartype/issues/87#issuecomment-1020856517
#FIXME: Fortuitously, implementing is_bearable() in terms of the existing
#@beartype decorator is trivial and requires absolutely *NO* refactoring of the
#"beartype" codebase itself, which is certainly nice (albeit non-essential):
#* Internally, is_bearable() should maintain a *non-LRU* cache (probably in a
#  separate "_bearable._cache" submodule as a simple dictionary) named
#  "HINT_OR_HINT_REPR_TO_BEARTYPE_WRAPPER" mapping from each arbitrary
#  PEP-compliant type hint (passed as the second parameter to is_bearable()) to
#  the corresponding wrapper function dynamically generated by the @beartype
#  decorator checking an arbitrary object against that hint. However, note
#  there there's a significant caveat here:
#  * *NOT ALL HINTS ARE CACHABLE.* If the passed hint is *NOT* cachable, we
#    should instead cache that hint under its machine-readable repr() string.
#    While slower to generate, generating that string is still guaranteed to be
#    *MUCH* faster than dynamically declaring a new function each call.
#* The trivial way to implement the prior item is to dynamically define one new
#  private @beartype-decorated noop function accepting an arbitrary parameter
#  type-hinted by each type hint: e.g.,
#      # Pseudo-code, obviously. Again, this snippet should probably be
#      # shifted into a new "_bearable._snip" submodule.
#      is_typed_as_wrapper = exec(f'''
#      @beartype
#      def is_typed_as_wrapper(obj: {hint}): pass
#      ''')
#* After either defining and caching that wrapper into the above dictionary
#  *OR* retrieved a previously wrapper from that dictionary, trivially
#  implement this check with EAFP as follows:
#      try:
#          is_typed_as_wrapper(obj)
#          return True
#      except:
#          return False
#
#*DONE.* Sweet, yah? The above can and should be heavily optimized, of course.
#How? That remains to be determined. The principle issue with the above
#approach is that it unnecessarily incurs an additional stack frame. Since the
#original is_typed_as_wrapper() function wrapped by @beartype doesn't actually
#do anything, it would be really nice if the wrapper generated by @beartype
#omitted the call to that original function.
#
#This might be easier than expected. You're probably thinking AST inspector or
#disassembly, right? Neither of those two things are easy or fast, so let's do
#neither. Is there any alternative? There might be. In theory, the code object
#for any callable whose implementation is literally "pass" should be trivially
#detectable via metadata on that object. If nothing else, the byte code for
#that object should be a constant size; any code object whose byte code is
#larger than that size is *NOT* a "pass" noop.
#
#In any case, @beartype should efficiently detect noop callables and avoid
#calling those callables from the wrapper functions it generates for those
#callables. This would be genuinely useful from the general-purpose
#perspective, which means we should make this happen.

# ....................{ IMPORTS                           }....................
from beartype import BeartypeConf, beartype
from beartype.roar import (
    BeartypeAbbyHintViolation,
    BeartypeCallHintReturnViolation,
)
from beartype.typing import Callable
from beartype._util.hint.utilhinttest import die_unless_hint

# ....................{ PRIVATE ~ constants               }....................
_TYPE_CHECKER_EXCEPTION_MESSAGE_PREFIX = (
    '@beartyped _get_type_checker._die_if_unbearable() return ')
'''
Irrelevant substring prefixing *all* exception messages raised by *all*
**runtime type-checkers** (i.e., functions created and returned by the
:func: `_get_type_checker` getter).
'''


_TYPE_CHECKER_EXCEPTION_MESSAGE_PREFIX_LEN = (
    len(_TYPE_CHECKER_EXCEPTION_MESSAGE_PREFIX))
'''
Length of the irrelevant substring prefixing *all*
exception messages raised by *all* **runtime type-checkers** (i.e., functions
created and returned by the :func: `_get_type_checker` getter).
'''

# ....................{ PRIVATE ~ hints                   }....................
_BeartypeTypeChecker = Callable[[object], None]
'''
PEP-compliant type hint matching a **runtime type-checker** (i.e., function
created and returned by the :func:`_get_type_checker` getter, raising a
:exc:`BeartypeCallHintReturnViolation` exception when the object passed to that
function violates a PEP-compliant type hint).
'''

# ....................{ VALIDATORS                        }....................
def die_if_unbearable(
    # Mandatory flexible parameters.
    obj: object,
    hint: object,

    # Optional keyword-only parameters.
    *, conf: BeartypeConf = BeartypeConf(),
) -> None:
    '''
    Raise an exception if the passed arbitrary object violates the passed
    PEP-compliant type hint.

    Parameters
    ----------
    obj : object
        Arbitrary object to be tested against this hint.
    hint : object
        PEP-compliant type hint to test this object against.
    conf : BeartypeConf, optional
        **Beartype configuration** (i.e., self-caching dataclass encapsulating
        all flags, options, settings, and other metadata configuring how this
        object is type-checked). Defaults to ``BeartypeConf()``, the default
        beartype configuration.

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

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

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

    # @beartype-decorated closure raising an
    # "BeartypeCallHintReturnViolation" exception if the parameter passed to
    # this closure violates the hint passed to this parent tester.
    _die_if_unbearable = _get_type_checker(hint, conf)

    # Attempt to type-check this object by passing this object to this closure,
    # which then implicitly type-checks this object as a return value.
    try:
        _die_if_unbearable(obj)
    # If this closure raises an exception as this object violates this hint...
    except BeartypeCallHintReturnViolation as exception:
        # Exception message.
        exception_message = str(exception)

        # Replace the irrelevant substring prefixing this message with a
        # relevant substring applicable to this higher-level function.
        exception_message = (
            f'Object '
            f'{exception_message[_TYPE_CHECKER_EXCEPTION_MESSAGE_PREFIX_LEN:]}'
        )

        # Wrap this exception in a more readable higher-level exception.
        raise BeartypeAbbyHintViolation(exception_message) from exception
    # Else, this closure raised another exception. In this case, percolate this
    # exception back up this call stack.

# ....................{ TESTERS                           }....................
def is_bearable(
    # Mandatory flexible parameters.
    obj: object,
    hint: object,

    # Optional keyword-only parameters.
    *, conf: BeartypeConf = BeartypeConf(),
) -> bool:
    '''
    ``True`` only if the passed arbitrary object satisfies the passed
    PEP-compliant type hint.

    Parameters
    ----------
    obj : object
        Arbitrary object to be tested against this hint.
    hint : object
        PEP-compliant type hint to test this object against.
    conf : BeartypeConf, optional
        **Beartype configuration** (i.e., self-caching dataclass encapsulating
        all flags, options, settings, and other metadata configuring how this
        object is type-checked). Defaults to ``BeartypeConf()``, the default
        beartype configuration.

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

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

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

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

    # @beartype-decorated closure raising an
    # "BeartypeCallHintReturnViolation" exception if the parameter passed to
    # this closure violates the hint passed to this parent tester.
    _die_if_unbearable = _get_type_checker(hint, conf)

    # Attempt to...
    try:
        # Type-check this object by passing this object to this closure, which
        # then implicitly type-checks this object as a return value.
        _die_if_unbearable(obj)

        # If this closure fails to raise an exception, this object *MUST*
        # necessarily satisfy this hint. In this case, return true.
        return True
    # If this closure raises an exception as this object violates this hint,
    # silently squelch this exception and return false below.
    except BeartypeCallHintReturnViolation:
        pass
    # Else, this closure raised another exception. In this case, percolate this
    # exception back up this call stack.

    # Return false, since this object violates this hint. (See above.)
    return False

# ....................{ PRIVATE ~ getters                 }....................
def _get_type_checker(
    hint: object, conf: BeartypeConf) -> _BeartypeTypeChecker:
    '''
    Create, cache, and return a **runtime type-checker** (i.e., function
    raising a :exc:`BeartypeCallHintReturnViolation` exception when the object
    passed to that function violates the hint passed to this parent getter
    under the passed beartype configuration).

    Note that this runtime type checker intentionally raises
    :exc:`BeartypeCallHintReturnViolation` rather than
    :exc:`BeartypeCallHintParamViolation` exceptions. Type-checking return
    values is marginally faster than type-checking parameters. Ergo, we
    intentionally annotate this return rather than parameter of this checker.

    Parameters
    ----------
    hint : object
        PEP-compliant type hint to validate all objects passed to this runtime
        type-checker against.
    conf : BeartypeConf, optional
        **Beartype configuration** (i.e., self-caching dataclass encapsulating
        all flags, options, settings, and other metadata configuring how this
        object is type-checked). Defaults to ``BeartypeConf()``, the default
        beartype configuration.

    Returns
    ----------
    _BeartypeTypeChecker
        Runtime type-checker specific to this hint and configuration.

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

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

    # If this hint is unsupported, raise an exception.
    #
    # Note that this technically duplicates a similar check performed by the
    # @beartype decorator below except that the exception prefix passed here
    # results in substantially more readable and relevant exceptions.
    die_unless_hint(hint=hint, exception_prefix='Functional ')
    # Else, this hint is supported.

    # @beartype-decorated closure raising an
    # "BeartypeCallHintReturnViolation" exception if the parameter passed to
    # this closure violates the hint passed to this parent tester.
    @beartype(conf=conf)
    def _die_if_unbearable(pith) -> hint:  # type: ignore[valid-type]
        return pith

    # Return this closure.
    return _die_if_unbearable
