diff --git a/beartype/_util/hint/pep/proposal/pep484585/utilpep484585func.py b/beartype/_util/hint/pep/proposal/pep484585/utilpep484585func.py index 3bc7d89f..225f397e 100644 --- a/beartype/_util/hint/pep/proposal/pep484585/utilpep484585func.py +++ b/beartype/_util/hint/pep/proposal/pep484585/utilpep484585func.py @@ -14,11 +14,13 @@ # ....................{ IMPORTS }.................... from beartype.roar import BeartypeDecorHintPep484585Exception +from beartype._data.datatyping import TypeException from beartype._data.hint.pep.sign.datapepsigns import HintSignCoroutine from beartype._data.hint.pep.sign.datapepsignset import ( HINT_SIGNS_RETURN_GENERATOR_ASYNC, HINT_SIGNS_RETURN_GENERATOR_SYNC, ) +from beartype._util.cls.utilclstest import is_type_subclass from beartype._util.func.utilfunctest import ( is_func_coro, is_func_async_generator, @@ -29,8 +31,11 @@ from beartype._util.hint.pep.utilpepget import get_hint_pep_sign_or_none from beartype._util.text.utiltextlabel import ( prefix_callable_decorated_return) -from beartype._data.datatyping import TypeException -from collections.abc import Callable +from collections.abc import ( + AsyncGenerator, + Callable, + Generator, +) # ....................{ REDUCERS ~ return }.................... def reduce_hint_pep484585_func_return( @@ -105,10 +110,17 @@ def reduce_hint_pep484585_func_return( # # If the decorated callable is an asynchronous generator... elif is_func_async_generator(func): - #FIXME: Unit test this up, please! - # If this hint is semantically invalid as the return annotation of this - # callable, raise an exception. - if hint_sign not in HINT_SIGNS_RETURN_GENERATOR_ASYNC: + # If this hint is neither... + if not ( + # A PEP-compliant type hint acceptable as the return annotation of + # an synchronous generator *NOR*... + hint_sign in HINT_SIGNS_RETURN_GENERATOR_ASYNC or + # The "collections.abc.AsyncGenerator" abstract base class (ABC) or + # a subclass of that ABC... + is_type_subclass(hint, AsyncGenerator) + # Then this hint is semantically invalid as the return annotation of + # this callable. In this case, raise an exception. + ): _die_of_hint_return_invalid( func=func, exception_suffix=( @@ -127,10 +139,17 @@ def reduce_hint_pep484585_func_return( # # If the decorated callable is a synchronous generator... elif is_func_sync_generator(func): - #FIXME: Unit test this up, please! - # If this hint is semantically invalid as the return annotation of this - # callable, raise an exception. - if hint_sign not in HINT_SIGNS_RETURN_GENERATOR_SYNC: + # If this hint is neither... + if not ( + # A PEP-compliant type hint acceptable as the return annotation of a + # synchronous generator *NOR*... + hint_sign in HINT_SIGNS_RETURN_GENERATOR_SYNC or + # The "collections.abc.Generator" abstract base class (ABC) or a + # subclass of that ABC... + is_type_subclass(hint, Generator) + # Then this hint is semantically invalid as the return annotation of + # this callable. In this case, raise an exception. + ): _die_of_hint_return_invalid( func=func, exception_suffix=( @@ -191,5 +210,5 @@ def _die_of_hint_return_invalid( # Raise an exception of this type with a message suffixed by this suffix. raise exception_cls( f'{prefix_callable_decorated_return(func)}type hint ' - f'{repr(hint)} contextually invalid{exception_suffix}.' + f'{repr(hint)} contextually invalid{exception_suffix}' ) diff --git a/beartype_test/a00_unit/a90_decor/a40_code/a90_pep/test_pep484585.py b/beartype_test/a00_unit/a90_decor/a40_code/a90_pep/test_pep484585.py index 5b01457a..0e03af4a 100644 --- a/beartype_test/a00_unit/a90_decor/a40_code/a90_pep/test_pep484585.py +++ b/beartype_test/a00_unit/a90_decor/a40_code/a90_pep/test_pep484585.py @@ -27,6 +27,7 @@ async def test_decor_async_coroutine() -> None: Test decorating coroutines with the :func:`beartype.beartype` decorator. ''' + # ....................{ IMPORTS }.................... # Defer test-specific imports. from asyncio import sleep from beartype import beartype @@ -36,6 +37,7 @@ async def test_decor_async_coroutine() -> None: from collections.abc import Coroutine as Pep585Coroutine from typing import Union, Coroutine as Pep484Coroutine + # ....................{ LOCALS }.................... # Decorated coroutine whose return is annotated with an arbitrary # PEP-compliant type hint. @beartype @@ -46,13 +48,6 @@ async def control_the_car( await sleep(0) return said_the + biggest_greenest_bat - # Assert awaiting this coroutine 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.') - # Decorated coroutine whose return is annotated with a PEP 484-compliant # coroutine type hint. @beartype @@ -62,14 +57,24 @@ async def universal_love( await sleep(0) return said_the + cactus_person + # ....................{ PASS }.................... + # Assert awaiting this coroutine 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 awaiting this coroutine returns the expected value. assert await universal_love( 'The sea was made of strontium; ', 'the beach was made of rye.') == ( 'The sea was made of strontium; the beach was made of rye.') + # ....................{ VERSION }.................... # If the active Python interpreter targets Python >= 3.9 and thus supports # PEP 585... if IS_PYTHON_AT_LEAST_3_9: + # ....................{ LOCALS }.................... # Decorated coroutine whose return is annotated with a PEP # 585-compliant coroutine type hint. @beartype @@ -79,12 +84,14 @@ async def transcendent_joy( await sleep(0) return said_the + big_green_bat + # ....................{ PASS }.................... # Assert awaiting this coroutine returns the expected value. assert await transcendent_joy( 'A thousand stars of sertraline ', 'whirled round quetiapine moons' ) == 'A thousand stars of sertraline whirled round quetiapine moons' + # ....................{ FAIL }.................... # Assert @beartype raises the expected exception when decorating a # coroutine whose return is annotated with a PEP 585-compliant # coroutine type hint *NOT* subscripted by exactly three child hints. @@ -105,12 +112,16 @@ async def test_decor_async_generator() -> None: decorator. ''' + # ....................{ IMPORTS }.................... # Defer test-specific imports. from asyncio import sleep from beartype import beartype from beartype.roar import BeartypeDecorHintPep484585Exception from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9 from beartype_test._util.pytroar import raises_uncached + from beartype.typing import ( + AsyncGenerator as AsyncGeneratorUnsubscripted, + ) from collections.abc import ( AsyncGenerator as Pep585AsyncGenerator, AsyncIterable as Pep585AsyncIterable, @@ -123,21 +134,31 @@ async def test_decor_async_generator() -> None: AsyncIterator as Pep484AsyncIterator, ) + # ....................{ LOCALS }.................... # Decorated asynchronous generators whose returns are annotated with PEP # 484-compliant "AsyncGenerator[...]", "AsyncIterable[...]", and # "AsyncIterator[...]" type hints (respectively). + @beartype + async def some_kind_of_spiritual_thing( + said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( + AsyncGeneratorUnsubscripted): + await sleep(0) + yield said_the + bigger_greener_bat + @beartype async def not_splitting_numbers( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( Pep484AsyncGenerator[Union[str, float], None]): await sleep(0) yield said_the + bigger_greener_bat + @beartype async def chaos_never_comes_from_the_ministry_of_chaos( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( Pep484AsyncIterable[Union[str, float]]): await sleep(0) yield said_the + bigger_greener_bat + @beartype async def nor_void_from_the_ministry_of_void( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( @@ -145,6 +166,7 @@ async def nor_void_from_the_ministry_of_void( await sleep(0) yield said_the + bigger_greener_bat + # ....................{ PASS }.................... # Assert awaiting these asynchronous generators return the expected values. # Unlike synchronous generators, asynchronous generators are *NOT* actually # iterators and thus have *NO* clean analogue to the iter() and next() @@ -152,6 +174,15 @@ async def nor_void_from_the_ministry_of_void( # await not_splitting_number.__anext__() # See also this relevant StackOvelflow post: # https://stackoverflow.com/a/42561322/2809027 + async for some_kind_of_spiritual in some_kind_of_spiritual_thing( + 'I should be trying to do some kind of spiritual thing ', + 'involving radical acceptance and enlightenment and such.', + ): + assert some_kind_of_spiritual == ( + 'I should be trying to do some kind of spiritual thing ' + 'involving radical acceptance and enlightenment and such.' + ) + async for not_splitting_number in not_splitting_numbers( 'the sand sizzled sharp like cooking oil that hissed and sang and ', 'threatened to boil the octahedral dunes.', @@ -160,6 +191,7 @@ async def nor_void_from_the_ministry_of_void( 'the sand sizzled sharp like cooking oil that hissed and sang and ' 'threatened to boil the octahedral dunes.' ) + async for chaos in chaos_never_comes_from_the_ministry_of_chaos( 'The force of the blast went rattling past the bat and the beach, ', 'disturbing each,' @@ -168,6 +200,7 @@ async def nor_void_from_the_ministry_of_void( 'The force of the blast went rattling past the bat and the beach, ' 'disturbing each,' ) + async for void in nor_void_from_the_ministry_of_void( 'then made its way to a nearby bay of upside-down trees ', 'with their roots in the breeze and their branches underground.' @@ -177,6 +210,7 @@ async def nor_void_from_the_ministry_of_void( 'with their roots in the breeze and their branches underground.' ) + # ....................{ FAIL }.................... # Assert this decorator raises the expected exception when decorating an # asynchronous generator annotating its return as anything *EXCEPT* # "AsyncGenerator[...]", "AsyncIterable[...]", and "AsyncIterator[...]". @@ -187,9 +221,11 @@ async def upside_down_trees( await sleep(0) yield roots_in_the_breeze + branches_underground + # ....................{ VERSION }.................... # If the active Python interpreter targets Python >= 3.9 and thus supports # PEP 585... if IS_PYTHON_AT_LEAST_3_9: + # ....................{ LOCALS }.................... # Decorated asynchronous generators whose returns are annotated with # PEP 585-compliant "AsyncGenerator[...]", "AsyncIterable[...]", and # "AsyncIterator[...]" type hints (respectively). @@ -199,30 +235,35 @@ async def but_joining_mind( ) -> Pep585AsyncGenerator[Union[str, float], None]: await sleep(0) yield said_the + bigger_greener_bat + @beartype async def lovers_do_not_love_to_increase( said_the: Union[str, float], bigger_greener_bat: Union[str, int], ) -> Pep585AsyncIterable[Union[str, float]]: await sleep(0) yield said_the + bigger_greener_bat + async def the_amount_of_love_in_the_world( said_the: Union[str, float], bigger_greener_bat: Union[str, int], ) -> Pep585AsyncIterator[Union[str, float]]: await sleep(0) yield said_the + bigger_greener_bat + # ....................{ PASS }.................... # Assert awaiting these asynchronous generators return the expected # values. async for but_joining_time in but_joining_mind( 'A meteorite of pure delight ', 'struck the sea without a sound.'): assert but_joining_time == ( 'A meteorite of pure delight struck the sea without a sound.') + async for the_mind_that_thrills in lovers_do_not_love_to_increase( 'The sea turned hot ', 'and geysers shot up from the floor below.' ): assert the_mind_that_thrills == ( 'The sea turned hot and geysers shot up from the floor below.') + async for the_face_of_the_beloved in the_amount_of_love_in_the_world( 'First one of wine, then one of brine, ', 'then one more yet of turpentine, and we three stared at the show.' @@ -244,11 +285,15 @@ def test_decor_sync_generator() -> None: decorator. ''' + # ....................{ IMPORTS }.................... # Defer test-specific imports. from beartype import beartype from beartype.roar import BeartypeDecorHintPep484585Exception from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9 from beartype_test._util.pytroar import raises_uncached + from beartype.typing import ( + Generator as GeneratorUnsubscripted, + ) from collections.abc import ( Generator as Pep585Generator, Iterable as Pep585Iterable, @@ -261,35 +306,51 @@ def test_decor_sync_generator() -> None: Iterator as Pep484Iterator, ) + # ....................{ LOCALS }.................... # Decorated synchronous generators whose returns are annotated with PEP # 484-compliant "Generator[...]", "Iterable[...]", and "Iterator[...]" type # hints (respectively). + @beartype + def western_logocentric_stuff( + said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( + GeneratorUnsubscripted): + yield said_the + bigger_greener_bat + @beartype def not_facts_or_factors_or_factories( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( Pep484Generator[Union[str, float], None, None]): yield said_the + bigger_greener_bat + @beartype def not_to_seek( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( Pep484Iterable[Union[str, float]]): yield said_the + bigger_greener_bat + @beartype def not_to_follow( said_the: Union[str, int], bigger_greener_bat: Union[str, float]) -> ( Pep484Iterator[Union[str, float]]): yield said_the + bigger_greener_bat + # ....................{ PASS }.................... # Assert awaiting these synchronous generators yield the expected values # when iterated. + assert next(western_logocentric_stuff( + 'all my Western logocentric stuff ', 'about factoring numbers', + )) == 'all my Western logocentric stuff about factoring numbers' + assert next(not_facts_or_factors_or_factories( 'The watery sun began to run ', 'and it fell on the ground as rain.', )) == 'The watery sun began to run and it fell on the ground as rain.' + assert next(not_to_seek( 'At the sound of that, ', 'the big green bat started rotating in place.', )) == ( 'At the sound of that, the big green bat started rotating in place.') + assert next(not_to_follow( 'On its other side was a bigger greener bat, ', 'with an ancient, wrinkled face.' @@ -298,6 +359,7 @@ def not_to_follow( 'with an ancient, wrinkled face.' ) + # ....................{ FAIL }.................... # Assert this decorator raises the expected exception when decorating a # synchronous generator annotating its return as anything *EXCEPT* # "Generator[...]", "Iterable[...]", and "Iterator[...]". @@ -307,9 +369,11 @@ def GET_OUT_OF_THE_CAR( FOR_THE_LOVE_OF_GOD: str, FACTOR_THE_NUMBER: str) -> str: yield FOR_THE_LOVE_OF_GOD + FACTOR_THE_NUMBER + # ....................{ VERSION }.................... # If the active Python interpreter targets Python >= 3.9 and thus supports # PEP 585... if IS_PYTHON_AT_LEAST_3_9: + # ....................{ LOCALS }.................... # Decorated synchronous generators whose returns are annotated with PEP # 585-compliant "Generator[...]", "Iterable[...]", and "Iterator[...]" # type hints (respectively). @@ -318,17 +382,20 @@ def contact_with_the_abstract_attractor( said_the: Union[str, float], bigger_greener_bat: Union[str, int], ) -> Pep585Generator[Union[str, float], None, None]: yield said_the + bigger_greener_bat + @beartype def but_to_jump_forth_into_the_deep( said_the: Union[str, float], bigger_greener_bat: Union[str, int], ) -> Pep585Iterable[Union[str, float], None, None]: yield said_the + bigger_greener_bat + @beartype def not_to_grind_or_to_bind_or_to_seek( said_the: Union[str, float], bigger_greener_bat: Union[str, int], ) -> Pep585Iterator[Union[str, float], None, None]: yield said_the + bigger_greener_bat + # ....................{ PASS }.................... # Assert these synchronous generators yield the expected values when # iterated. assert next(contact_with_the_abstract_attractor( @@ -338,6 +405,7 @@ def not_to_grind_or_to_bind_or_to_seek( 'Then tree and beast all fled due east and ' 'the moon and stars shot south.' ) + assert next(but_to_jump_forth_into_the_deep( 'The big green bat started to turn around ', 'what was neither its x, y, or z axis,' @@ -345,6 +413,7 @@ def not_to_grind_or_to_bind_or_to_seek( 'The big green bat started to turn around ' 'what was neither its x, y, or z axis,' ) + assert next(not_to_grind_or_to_bind_or_to_seek( 'slowly rotating to reveal what was undoubtedly the biggest, ', 'greenest bat that I had ever seen, a bat bigger and greener '