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

'''
Beartype decorator :pep:`342`-compliant synchronous generator unit tests.

This submodule unit tests :pep:`342` support for synchronous generators
implemented in the :func:`beartype.beartype` decorator.
'''

# ....................{ IMPORTS                            }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To raise human-readable test errors, avoid importing from
# package-specific submodules at module scope.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
from beartype.roar import BeartypeDecorHintPep585DeprecationWarning
from beartype_test._util.mark.pytmark import ignore_warnings

# ....................{ TESTS                              }....................
# Prevent pytest from capturing and displaying all expected non-fatal
# beartype-specific warnings emitted by the @beartype decorator below. Blargh!
@ignore_warnings(BeartypeDecorHintPep585DeprecationWarning)
def test_decor_pep342_sync_generator_checked() -> None:
    '''
    Test :func:`beartype.beartype`-decorated synchronous generators with
    respect to core type-checking.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from beartype import beartype
    from beartype.roar import (
        BeartypeCallHintParamViolation,
        BeartypeDecorHintPep484585Exception,
    )
    from beartype_test._util.pytroar import raises_uncached
    from collections.abc import (
        Generator as Pep585Generator,
        Iterable as Pep585Iterable,
        Iterator as Pep585Iterator,
    )
    from inspect import isgeneratorfunction
    from typing import (
        Any,
        Union,
        Generator as Pep484Generator,
        Iterable as Pep484Iterable,
        Iterator as Pep484Iterator,
    )

    # ....................{ LOCALS                         }....................
    # Tuple of all PEP 484- or 585-compliant type hints annotating the returns
    # of synchronous generators covering all possible edge cases.
    HINTS_RETURN = (
        # ....................{ NON-PEP                    }....................
        # The unsubscripted "collections.abc.Generator" ABC is a
        # PEP-noncompliant type.
        Pep585Generator,

        # ....................{ PEP 484                    }....................
        # Ignorable object preventing code generation from type-checking the
        # asynchronous generator created and returned by that factory.
        Any,

        Pep484Generator[Union[str, float], None, None],
        Pep484Iterable[Union[str, float]],
        Pep484Iterator[Union[str, float]],

        # ....................{ PEP 585                    }....................
        Pep585Generator[Union[str, float], None, None],
        Pep585Iterable[Union[str, float]],
        Pep585Iterator[Union[str, float]],
    )

    # ....................{ PASS                           }....................
    # For each return type hint defined above...
    for hint_return in HINTS_RETURN:
        @beartype
        def western_logocentric_stuff(
            said_the: Union[str, int], bigger_greener_bat: Union[str, float]
        ) -> hint_return:
            '''
            :func:`beartype.beartype`-decorated synchronous generator whose
            return is annotated by a :pep:`484`- or :pep:`585`-compliant type
            hint deeply type-checking the value yielded by this generator.
            '''

            # Yield an arbitrary combination of the passed parameters.
            yield said_the + bigger_greener_bat

        # Assert that the high-level wrapper generated by the @beartype
        # decorator type-checking the low-level synchronous generator defined
        # above preserves "inspect"-based metadata describing this generator.
        # Specifically, assert that this wrapper is still a generator.
        assert isgeneratorfunction(western_logocentric_stuff) is True

        # Assert that iterating the synchronous generator created by passing
        # this function valid parameters yields the expected item.
        assert next(western_logocentric_stuff(
              'all my Western logocentric stuff ', 'about factoring numbers',
        )) == 'all my Western logocentric stuff about factoring numbers'

        # Assert that passing this factory invalid parameters raises the
        # expected violation.
        with raises_uncached(BeartypeCallHintParamViolation):
            next(western_logocentric_stuff(
                b'asking the entities you meet',
                b'to factor large numbers',
            ))

    # ....................{ FAIL                           }....................
    # Assert that @beartype raises the expected exception when decorating a
    # synchronous generator annotating its return as anything *EXCEPT*
    # "Generator[...]", "Iterable[...]", and "Iterator[...]".
    with raises_uncached(BeartypeDecorHintPep484585Exception):
        @beartype
        def GET_OUT_OF_THE_CAR(
            FOR_THE_LOVE_OF_GOD: str, FACTOR_THE_NUMBER: str) -> str:
            # Return an arbitrary object (sorta) satisfying this return hint.
            yield FOR_THE_LOVE_OF_GOD + FACTOR_THE_NUMBER


def test_decor_pep342_sync_generator_api() -> None:
    '''
    Test :func:`beartype.beartype`-decorated synchronous generators with
    respect to **bidirectional communication** (i.e., the pure-Python
    implementation of a hypothetical ``async yield from`` expression dynamically
    injected into the bodies of higher-level wrapper functions type-checking
    lower-level :func:`beartype.beartype`-decorated synchronous generator
    factories).

    This unit test validates that higher-level wrapper functions type-checking
    lower-level :func:`beartype.beartype`-decorated synchronous generator
    factories continue to comply with :pep:`342` semantics, despite the
    syntactic lack of a ``async yield from`` expression comparable to the
    existing :pep:`380`-compliant ``yield from`` expression. Specifically, this
    unit test validates comformance with:

    * :meth:`types.GeneratorType.close` semantics.
    * :meth:`types.GeneratorType.send` semantics.
    * :meth:`types.GeneratorType.throw` semantics.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from beartype import beartype
    from collections.abc import Generator
    from pytest import raises
    from typing import Any

    # ....................{ LOCALS                         }....................
    # Tuple of all return hints with which to repeatedly annotate the non-empty
    # synchronous generator factory declared below, each validating a unique
    # edge case in code generation for that factory.
    HINTS_RETURN = (
        # Ignorable object preventing code generation from type-checking the
        # synchronous generator created and returned by that factory.
        Any,

        # PEP 342-compliant return hunt instructing code generation to
        # type-check the yields, sends, *AND* returns of that generator.
        Generator[int, str, bytes],
    )

    # Arbitrary list to be appended to by the "finally:" block of the non-empty
    # synchronous generator why_do_i_know_ye() defined below, enabling logic
    # below to validate that that block was run as expected *AFTER* the caller
    # prematurely closes that generator by calling its close() method.
    to_see_and_to_behold = []

    # Arbitrary string to be inserted into this list below.
    HORRORS_NEW = 'To see and to behold these horrors new?'

    # Arbitrary byte string to be returned from the non-empty synchronous
    # generator why_do_i_know_ye() defined below.
    SATURN_FALLEN = b'Saturn is fallen, am I too to fall?'

    # ....................{ EXCEPTIONS                     }....................
    class _BeartypeGeneratorThrowException(Exception):
        '''
        Arbitrary exception synchronously thrown below into an synchronous
        generator during iteration.
        '''

        pass

    # ....................{ CALLABLES                      }....................
    @beartype
    def is_my_eternal_essence() -> Generator:
        '''
        :func:`beartype.beartype`-decorated synchronous generator guaranteed to
        finalize *before* yielding anything.

        See Also
        --------
        https://stackoverflow.com/a/13243870/2809027
            StackOverflow answer inspiring this implementation.
        '''

        # Immediately return *BEFORE* yielding. (Look. Don't ask.)
        return

        # Declare this function to be an synchronous generator factory rather
        # than an synchronous coroutine. Without this "yield" statement, this
        # function would instead be interpreted as the latter.
        #
        # This synchronous generator is guaranteed to return before yielding.
        # This synchronous generator is guaranteed to thus be empty.
        yield

    # ....................{ LOOP                           }....................
    # For each return hint with which to repeatedly annotate the non-empty
    # synchronous generator factory declared below...
    for hint_return in HINTS_RETURN:
        @beartype
        def why_do_i_know_ye(yield_int_max: int) -> hint_return:
            '''
            :func:`beartype.beartype`-decorated non-empty synchronous generator
            yielding:

            * When sent an arbitrary string, the length of that string.
            * Else, non-negative integers in the inclusive range
              ``[0, yield_int_max]``.

            This generator then yields a final byte string *after* exhaustion
            (i.e., yielding all of these values).

            Parameters
            ----------
            yield_int_max : int
                Maximum non-negative integer to be yielded.
            '''
            assert yield_int_max >= 0

            # Attempt to...
            try:
                # For each integer in this range to be yielded...
                for yield_int in range(yield_int_max + 1):
                    # Attempt to...
                    try:
                        # Yield this integer to the caller *AND* possibly
                        # capture a string sent by the caller into this
                        # generator.
                        sent_str = yield yield_int

                        # While the caller continues sending strings into this
                        # generator, yield the length of each such string as
                        # confirmation of receipt to the caller.
                        while sent_str is not None:
                            sent_str = yield len(sent_str)
                        # Else, the caller sent *NO* values into this generator.
                    # If the caller explicitly threw an exception into this
                    # generator by calling this generator's throw() method,
                    # yield the negation of the length of this exception's
                    # message as confirmation of receipt.
                    except _BeartypeGeneratorThrowException as exception:
                        yield -len(str(exception))  # <-- wat lol
            # Append an arbitrary object to the list defined above, enabling
            # logic below to validate this finalizer was run as expected after
            # the caller prematurely closes this generator with close().
            finally:
                to_see_and_to_behold.append(HORRORS_NEW)

            # Return an arbitrary object, enabling logic below to validate this
            # extremely uncommon (yet vital) edge case.
            return SATURN_FALLEN

        # ....................{ ASSERTS ~ empty : yield    }....................
        # Validate that a @beartype-decorated empty synchronous generator
        # factory preserves PEP 580-compliant "yield" semantics.

        # True only if this generator truly is empty.
        is_my_eternal_essence_empty = True

        # If this generator yields *ANY* values, note this generator to be
        # non-empty.
        for yielded_value in is_my_eternal_essence():
            is_my_eternal_essence_empty = False

        # Assert that this generator truly is empty.
        assert is_my_eternal_essence_empty is True

        # ....................{ ASSERTS ~ non-empty : yield}....................
        # Validate that a @beartype-decorated non-empty synchronous generator
        # factory preserves PEP 580-compliant "yield" semantics.

        # Synchronous generator produced by priming this factory with a
        # small (but still non-zero) maximum value to be yielded.
        am_i_too_to_fall = why_do_i_know_ye(3)

        # Assert that each value iteratively yielded by this generator is the
        # expected yield. To permit subsequent logic to capture the value
        # returned by this generator, this logic intentionally avoids iterating
        # over this generator with a "for" loop. Why? Because doing so would
        # implicitly capture the first "StopIteration" exception implicitly
        # raised by exhausting this generator. Since the first and *ONLY* the
        # first "StopIteration" exception implicitly raised by exhausting a
        # generator defines a "value" instance variable whose value is the value
        # returned by that generator, manual iteration is the *ONLY* means of
        # capturing generator returns. Insane... but that's generators for you.
        assert next(am_i_too_to_fall) == 0
        assert next(am_i_too_to_fall) == 1
        assert next(am_i_too_to_fall) == 2
        assert next(am_i_too_to_fall) == 3

        # ....................{ ASSERTS ~ non-empty : retur}....................
        # Validate that a @beartype-decorated non-empty synchronous generator
        # factory preserves PEP 580-compliant "return" semantics.

        # Assert that attempting to iterate this generator once raises the
        # expected PEP 342-compliant "StopIteration" exception.
        with raises(StopIteration) as exception_info:
            next(am_i_too_to_fall)

        # Exception encapsulated by this "pytest"-specific metadata, localized
        # simply for readability. "exception_info.value.value" reads
        # nonsensically. This is already inscrutable enough, folks.
        exception = exception_info.value

        # Assert that the value returned with this exception is the same byte
        # string returned by this generator above.
        assert exception.value is SATURN_FALLEN

        # Assert that this generator's "finally:" block finalized this generator
        # by appending the expected string to this closure list.
        assert to_see_and_to_behold == [HORRORS_NEW]

        # Clear this list for sanity.
        to_see_and_to_behold.clear()

        # ....................{ ASSERTS ~ non-empty : send }....................
        # Validate that a @beartype-decorated non-empty synchronous generator
        # factory preserves PEP 580-compliant send() semantics.

        # Synchronous generator produced by priming this factory as above.
        why_have_i_seen_ye = why_do_i_know_ye(3)
        # print(f'why_have_i_seen_ye: {dir(why_have_i_seen_ye)}')

        # Assert that the first value yielded by this generator is the expected.
        # Rather than iterate this generator manually via the standard idiom,
        # validate that sending the "None" singleton into this generator is
        # semantically equivalent to manually iterating this generator.
        #
        # Note that:
        # * This obtuse alternative to explicit iteration is equivalent to this
        #   standard idiom for manually iterating an synchronous generator:
        #     assert next(why_have_i_seen_ye) == 0
        # * One *CANNOT* send a non-"None" value into an synchronous generator
        #   before that generator yields its first value after being iterated at
        #   least once. Violating that maxim raises this exception:
        #     TypeError: can't send non-None value to a just-started async
        #     generator
        assert why_have_i_seen_ye.send(None) == 0

        # Assert that the next value yielded by this generator is the expected.
        assert next(why_have_i_seen_ye) == 1

        # Assert that the value yielded by this generator after sending a string
        # into this generator is the length of this string.
        assert why_have_i_seen_ye.send(
            'Why do I know ye? why have I seen ye? why') == 41

        # Assert that the value yielded by this generator after sending another
        # string into this generator is the length of this string.
        assert why_have_i_seen_ye.send(
            'Is my eternal essence thus distraught') == 37

        # Assert that the next value yielded by this generator is the expected.
        assert next(why_have_i_seen_ye) == 2

        # ....................{ ASSERTS ~ non-empty : throw}....................
        # Assert that the value yielded by this generator after throwing an
        # exception into this generator is the negation of the length of this
        # exception's message. Just accept it.
        assert why_have_i_seen_ye.throw(
            _BeartypeGeneratorThrowException(
                'To see and to behold these horrors new?')) == -39

        # ....................{ ASSERTS ~ non-empty : send }....................
        # Assert that the final value yielded by this generator is the expected.
        assert next(why_have_i_seen_ye) == 3

        # Assert that iterating this generator one final time raises the
        # expected PEP 342-compliant exception -- implying this generator has
        # nothing further to yield and has thus been exhausted (finalized).
        with raises(StopIteration):
            next(why_have_i_seen_ye)

        # Assert that this generator's "finally:" block finalized this generator
        # by appending the expected string to this closure list.
        assert to_see_and_to_behold == [HORRORS_NEW]

        # Clear this list for sanity.
        to_see_and_to_behold.clear()

        # ....................{ ASSERTS ~ non-empty : close}....................
        # hronous generator produced by priming this factory as above.
        thus_distraught = why_do_i_know_ye(3)

        # Assert that the first value yielded by this generator is the expected.
        assert next(thus_distraught) == 0

        # Prematurely close this generator.
        thus_distraught.close()

        # Assert that this generator's "finally:" block finalized this generator
        # by appending the expected string to this closure list.
        assert to_see_and_to_behold == [HORRORS_NEW]

        # Clear this list for sanity.
        to_see_and_to_behold.clear()

        # Assert that iterating this generator one final time raises the
        # expected PEP 342-compliant exception -- implying this generator has
        # nothing further to yield and has thus been exhausted (finalized).
        with raises(StopIteration):
            next(thus_distraught)
