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

'''
Beartype decorator :pep:`484`- and :pep:`585`-compliant unit tests.

This submodule unit tests :pep:`484` and :pep:`585` support 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 ~ sync                       }....................
# 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_sync_generator() -> None:
    '''
    Test decorating synchronous generators with the :func:`beartype.beartype`
    decorator.
    '''

    # ....................{ 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 (
        Union,
        Generator as Pep484Generator,
        Iterable as Pep484Iterable,
        Iterator as Pep484Iterator,
    )

    # ....................{ CALLABLES                      }....................
    @beartype
    def transcendent_joy(
        said_the: Union[str, float], big_green_bat: Union[str, int]):
        '''
        :func:`beartype.beartype`-decorated synchronous generator whose return
        is intentionally left unannotated, validating a common edge case.
        '''

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

    # ....................{ 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                    }....................
        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 iterating the synchronous generator created by passing
        # this function 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'))

    # 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.
    assert isgeneratorfunction(transcendent_joy) is True

    # Assert that iterating the synchronous generator created by passing this
    # function valid parameters yields the expected item.
    assert next(transcendent_joy(
        'First one of wine, then one of brine, ',
        'then one more yet of turpentine,',
    )) == (
        'First one of wine, then one of brine, '
        'then one more yet of turpentine,'
    )

    # ....................{ FAIL                           }....................
    # Assert that iterating the synchronous generator created by passing this
    # function invalid parameters raises the expected violation.
    with raises_uncached(BeartypeCallHintParamViolation):
        next(transcendent_joy(
            b'Not facts or factors or factories',
            b'but contact with the abstract attractor that brings you back to me'
        ))

    # 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

# ....................{ TESTS ~ async                      }....................
# Prevent pytest from capturing and displaying all expected non-fatal
# beartype-specific warnings emitted by the @beartype decorator below. Urgh!
@ignore_warnings(BeartypeDecorHintPep585DeprecationWarning)
async def test_decor_async_coroutine() -> None:
    '''
    Test decorating **coroutines** (i.e., asynchronous non-generator functions)
    with the :func:`beartype.beartype` decorator.
    '''

    # ....................{ IMPORTS                        }....................
    # Defer test-specific imports.
    from asyncio import sleep
    from beartype import beartype
    from beartype.roar import (
        BeartypeCallHintParamViolation,
        BeartypeDecorHintPep585Exception,
    )
    from beartype_test._util.pytroar import raises_uncached
    from collections.abc import Coroutine as Pep585Coroutine
    from inspect import (
        isasyncgenfunction,
        iscoroutinefunction,
    )
    from typing import (
        Union,
        Coroutine as Pep484Coroutine,
    )

    # ....................{ LOCALS                         }....................
    # Tuple of all PEP 484- or 585-compliant type hints annotating the returns
    # of synchronous generators covering all possible edge cases.
    HINTS_RETURN = (
        # ....................{ PEP 484                    }....................
        Pep484Coroutine[None, None, Union[str, float]],
        Union[str, float],

        # ....................{ PEP 585                    }....................
        Pep585Coroutine[None, None, Union[str, float]],
    )

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

            # Silently reduce to an asynchronous noop. *ALL* coroutines are
            # required to call the "await" keyword at least once. Since the
            # placeholder object returned below is synchronous and thus *CANNOT*
            # be asynchronously awaited, this fallback wrapper has *NO* recourse
            # but to asynchronously await a minimal-cost awaitable. Aaaaaaand...
            #
            # This is why the "asyncio" API is Python's most hated. We sigh!
            await sleep(0)

            # Return an arbitrary combination of the passed parameters.
            return said_the + biggest_greenest_bat

        # Assert that the high-level wrapper generated by the @beartype
        # decorator type-checking the low-level coroutine defined above
        # preserves "inspect"-based metadata describing this coroutine.
        # Specifically, assert that this wrapper is still a coroutine but has
        # *NOT* been improperly transformed into an asynchronous generator.
        assert iscoroutinefunction(control_the_car) is True
        assert isasyncgenfunction(control_the_car) is False

        # Assert that awaiting the coroutine created by passing this function
        # valid parameters returns the expected value.
        assert await control_the_car(
            'I saw the big green bat bat a green big eye. ',
            'Suddenly I knew I had gone too far.') == (
            'I saw the big green bat bat a green big eye. '
            'Suddenly I knew I had gone too far.')

        # Assert that awaiting the coroutine created by passing this function
        # invalid parameters raises the expected violation.
        with raises_uncached(BeartypeCallHintParamViolation):
            await control_the_car(
                b'The sea was made of strontium;',
                b'the beach was made of rye.',
            )

    # ....................{ FAIL                           }....................
    # Assert that @beartype raises the expected exception when decorating a
    # coroutine whose return is annotated by a PEP 585-compliant coroutine type
    # hint *NOT* subscripted by exactly three child hints.
    with raises_uncached(BeartypeDecorHintPep585Exception):
        @beartype
        async def with_each_planck_moment_ever_fit() -> (
            Pep585Coroutine['to be eternally enjoyed']):
            # Silently reduce to an asynchronous noop. See above for details.
            await sleep(0)

            # Return an arbitrary object (sorta) satisfying this return hint.
            return 'Time will decay us but time can be left blank'
