diff --git a/setup.cfg b/setup.cfg index e2bbef1d..638fe6d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ setup_requires = pytest-runner install_requires = decopatch - makefun>=1.9.5 + makefun>=1.15.1 packaging # note: pytest, too :) functools32;python_version<'3.2' diff --git a/src/pytest_cases/fixture_core1_unions.py b/src/pytest_cases/fixture_core1_unions.py index 9ff239e5..af5582bd 100644 --- a/src/pytest_cases/fixture_core1_unions.py +++ b/src/pytest_cases/fixture_core1_unions.py @@ -10,12 +10,26 @@ from makefun import with_signature, add_signature_parameters, wraps import pytest +import sys try: # python 3.3+ from inspect import signature, Parameter except ImportError: from funcsigs import signature, Parameter # noqa +try: # native coroutines, python 3.5+ + from inspect import iscoroutinefunction +except ImportError: + def iscoroutinefunction(obj): + return False + +try: # native async generators, python 3.6+ + from inspect import isasyncgenfunction +except ImportError: + def isasyncgenfunction(obj): + return False + + try: # type hints, python 3+ from typing import Callable, Union, Optional, Any, List, Iterable, Sequence # noqa from types import ModuleType # noqa @@ -224,7 +238,27 @@ def ignore_unused(fixture_func): else: new_sig = old_sig - if not isgeneratorfunction(fixture_func): + if isasyncgenfunction(fixture_func) and sys.version_info >= (3, 6): + from .pep525 import _ignore_unused_asyncgen_pep525 + wrapped_fixture_func = _ignore_unused_asyncgen_pep525(fixture_func, new_sig, func_needs_request) + elif iscoroutinefunction(fixture_func) and sys.version_info >= (3, 5): + from .pep492 import _ignore_unused_coroutine_pep492 + wrapped_fixture_func = _ignore_unused_coroutine_pep492(fixture_func, new_sig, func_needs_request) + elif isgeneratorfunction(fixture_func): + if sys.version_info >= (3, 3): + from .pep380 import _ignore_unused_generator_pep380 + wrapped_fixture_func = _ignore_unused_generator_pep380(fixture_func, new_sig, func_needs_request) + else: + # generator function (with a yield statement) + @wraps(fixture_func, new_sig=new_sig) + def wrapped_fixture_func(*args, **kwargs): + request = kwargs['request'] if func_needs_request else kwargs.pop('request') + if is_used_request(request): + for res in fixture_func(*args, **kwargs): + yield res + else: + yield NOT_USED + else: # normal function with return statement @wraps(fixture_func, new_sig=new_sig) def wrapped_fixture_func(*args, **kwargs): @@ -234,17 +268,6 @@ def wrapped_fixture_func(*args, **kwargs): else: return NOT_USED - else: - # generator function (with a yield statement) - @wraps(fixture_func, new_sig=new_sig) - def wrapped_fixture_func(*args, **kwargs): - request = kwargs['request'] if func_needs_request else kwargs.pop('request') - if is_used_request(request): - for res in fixture_func(*args, **kwargs): - yield res - else: - yield NOT_USED - return wrapped_fixture_func diff --git a/src/pytest_cases/fixture_core2.py b/src/pytest_cases/fixture_core2.py index 0655f817..43b355e6 100644 --- a/src/pytest_cases/fixture_core2.py +++ b/src/pytest_cases/fixture_core2.py @@ -12,12 +12,25 @@ from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps import pytest +import sys try: # python 3.3+ from inspect import signature, Parameter except ImportError: from funcsigs import signature, Parameter # noqa +try: # native coroutines, python 3.5+ + from inspect import iscoroutinefunction +except ImportError: + def iscoroutinefunction(obj): + return False + +try: # native async generators, python 3.6+ + from inspect import isasyncgenfunction +except ImportError: + def isasyncgenfunction(obj): + return False + try: # type hints, python 3+ from typing import Callable, Union, Any, List, Iterable, Sequence # noqa from types import ModuleType # noqa @@ -528,7 +541,27 @@ def _map_arguments(*_args, **_kwargs): return _args, _kwargs # --Finally create the fixture function, a wrapper of user-provided fixture with the new signature - if not isgeneratorfunction(fixture_func): + if isasyncgenfunction(fixture_func)and sys.version_info >= (3, 6): + from .pep525 import _decorate_fixture_plus_asyncgen_pep525 + wrapped_fixture_func = _decorate_fixture_plus_asyncgen_pep525(fixture_func, new_sig, _map_arguments) + elif iscoroutinefunction(fixture_func) and sys.version_info >= (3, 5): + from .pep492 import _decorate_fixture_plus_coroutine_pep492 + wrapped_fixture_func = _decorate_fixture_plus_coroutine_pep492(fixture_func, new_sig, _map_arguments) + elif isgeneratorfunction(fixture_func): + # generator function (with a yield statement) + if sys.version_info >= (3, 3): + from .pep380 import _decorate_fixture_plus_generator_pep380 + wrapped_fixture_func = _decorate_fixture_plus_generator_pep380(fixture_func, new_sig, _map_arguments) + else: + @wraps(fixture_func, new_sig=new_sig) + def wrapped_fixture_func(*_args, **_kwargs): + if not is_used_request(_kwargs['request']): + yield NOT_USED + else: + _args, _kwargs = _map_arguments(*_args, **_kwargs) + for res in fixture_func(*_args, **_kwargs): + yield res + else: # normal function with return statement @wraps(fixture_func, new_sig=new_sig) def wrapped_fixture_func(*_args, **_kwargs): @@ -538,17 +571,6 @@ def wrapped_fixture_func(*_args, **_kwargs): _args, _kwargs = _map_arguments(*_args, **_kwargs) return fixture_func(*_args, **_kwargs) - else: - # generator function (with a yield statement) - @wraps(fixture_func, new_sig=new_sig) - def wrapped_fixture_func(*_args, **_kwargs): - if not is_used_request(_kwargs['request']): - yield NOT_USED - else: - _args, _kwargs = _map_arguments(*_args, **_kwargs) - for res in fixture_func(*_args, **_kwargs): - yield res - # transform the created wrapper into a fixture _make_fix = pytest_fixture(scope=scope, params=final_values, autouse=autouse, hook=hook, ids=final_ids, **kwargs) return _make_fix(wrapped_fixture_func) diff --git a/src/pytest_cases/fixture_parametrize_plus.py b/src/pytest_cases/fixture_parametrize_plus.py index 644ebefd..d70159c2 100644 --- a/src/pytest_cases/fixture_parametrize_plus.py +++ b/src/pytest_cases/fixture_parametrize_plus.py @@ -11,6 +11,18 @@ except ImportError: from funcsigs import signature, Parameter # noqa +try: # native coroutines, python 3.5+ + from inspect import iscoroutinefunction +except ImportError: + def iscoroutinefunction(obj): + return False + +try: # native async generators, python 3.6+ + from inspect import isasyncgenfunction +except ImportError: + def isasyncgenfunction(obj): + return False + try: from collections.abc import Iterable except ImportError: # noqa @@ -25,6 +37,7 @@ pass import pytest +import sys from makefun import with_signature, remove_signature_parameters, add_signature_parameters, wraps from .common_mini_six import string_types @@ -1059,30 +1072,44 @@ def replace_paramfixture_with_values(kwargs): # noqa # return return kwargs - if not isgeneratorfunction(test_func): - # normal test or fixture function with return statement - @wraps(test_func, new_sig=new_sig) - def wrapped_test_func(*args, **kwargs): # noqa - if kwargs.get(fixture_union_name, None) is NOT_USED: - # TODO why this ? it is probably useless: this fixture - # is private and will never end up in another union - return NOT_USED - else: - replace_paramfixture_with_values(kwargs) - return test_func(*args, **kwargs) + if isasyncgenfunction(test_func)and sys.version_info >= (3, 6): + from .pep525 import _parametrize_plus_decorate_asyncgen_pep525 + wrapped_test_func = _parametrize_plus_decorate_asyncgen_pep525(test_func, new_sig, fixture_union_name, + replace_paramfixture_with_values) + elif iscoroutinefunction(test_func) and sys.version_info >= (3, 5): + from .pep492 import _parametrize_plus_decorate_coroutine_pep492 + wrapped_test_func = _parametrize_plus_decorate_coroutine_pep492(test_func, new_sig, fixture_union_name, + replace_paramfixture_with_values) + elif isgeneratorfunction(test_func): + # generator function (with a yield statement) + if sys.version_info >= (3, 3): + from .pep380 import _parametrize_plus_decorate_generator_pep380 + wrapped_test_func = _parametrize_plus_decorate_generator_pep380(test_func, new_sig, + fixture_union_name, + replace_paramfixture_with_values) + else: + @wraps(test_func, new_sig=new_sig) + def wrapped_test_func(*args, **kwargs): # noqa + if kwargs.get(fixture_union_name, None) is NOT_USED: + # TODO why this ? it is probably useless: this fixture + # is private and will never end up in another union + yield NOT_USED + else: + replace_paramfixture_with_values(kwargs) + for res in test_func(*args, **kwargs): + yield res else: - # generator test or fixture function (with one or several yield statements) + # normal function with return statement @wraps(test_func, new_sig=new_sig) def wrapped_test_func(*args, **kwargs): # noqa if kwargs.get(fixture_union_name, None) is NOT_USED: # TODO why this ? it is probably useless: this fixture # is private and will never end up in another union - yield NOT_USED + return NOT_USED else: replace_paramfixture_with_values(kwargs) - for res in test_func(*args, **kwargs): - yield res + return test_func(*args, **kwargs) # move all pytest marks from the test function to the wrapper # not needed because the __dict__ is automatically copied when we use @wraps diff --git a/src/pytest_cases/pep380.py b/src/pytest_cases/pep380.py new file mode 100644 index 00000000..fc9bee57 --- /dev/null +++ b/src/pytest_cases/pep380.py @@ -0,0 +1,50 @@ +# Authors: Sylvain MARIE +# + All contributors to +# +# License: 3-clause BSD, + +# contains syntax illegal before PEP380 'Syntax for Delegating to a Subgenerator' + +from makefun import wraps +from .fixture_core1_unions import is_used_request, NOT_USED + + +def _ignore_unused_generator_pep380(fixture_func, new_sig, func_needs_request): + @wraps(fixture_func, new_sig=new_sig) + def wrapped_fixture_func(*args, **kwargs): + request = kwargs['request'] if func_needs_request else kwargs.pop('request') + if is_used_request(request): + yield from fixture_func(*args, **kwargs) + else: + yield NOT_USED + + return wrapped_fixture_func + +def _decorate_fixture_plus_generator_pep380(fixture_func, new_sig, map_arguments): + @wraps(fixture_func, new_sig=new_sig) + def wrapped_fixture_func(*_args, **_kwargs): + if not is_used_request(_kwargs['request']): + yield NOT_USED + else: + _args, _kwargs = map_arguments(*_args, **_kwargs) + yield from fixture_func(*_args, **_kwargs) + + return wrapped_fixture_func + +def _parametrize_plus_decorate_generator_pep380( + test_func, + new_sig, + fixture_union_name, + replace_paramfixture_with_values +): + @wraps(test_func, new_sig=new_sig) + def wrapped_test_func(*args, **kwargs): # noqa + if kwargs.get(fixture_union_name, None) is NOT_USED: + # TODO why this ? it is probably useless: this fixture + # is private and will never end up in another union + yield NOT_USED + else: + replace_paramfixture_with_values(kwargs) + yield from test_func(*args, **kwargs) + + return wrapped_test_func diff --git a/src/pytest_cases/pep492.py b/src/pytest_cases/pep492.py new file mode 100644 index 00000000..f4e2528e --- /dev/null +++ b/src/pytest_cases/pep492.py @@ -0,0 +1,50 @@ +# Authors: Sylvain MARIE +# + All contributors to +# +# License: 3-clause BSD, + +# contains syntax illegal before PEP492 "Coroutines with async and await syntax" + +from makefun import wraps +from .fixture_core1_unions import is_used_request, NOT_USED + + +def _ignore_unused_coroutine_pep492(fixture_func, new_sig, func_needs_request): + @wraps(fixture_func, new_sig=new_sig) + async def wrapped_fixture_func(*args, **kwargs): + request = kwargs['request'] if func_needs_request else kwargs.pop('request') + if is_used_request(request): + return await fixture_func(*args, **kwargs) + else: + return NOT_USED + + return wrapped_fixture_func + +def _decorate_fixture_plus_coroutine_pep492(fixture_func, new_sig, map_arguments): + @wraps(fixture_func, new_sig=new_sig) + async def wrapped_fixture_func(*_args, **_kwargs): + if not is_used_request(_kwargs['request']): + return NOT_USED + else: + _args, _kwargs = map_arguments(*_args, **_kwargs) + return await fixture_func(*_args, **_kwargs) + + return wrapped_fixture_func + +def _parametrize_plus_decorate_coroutine_pep492( + test_func, + new_sig, + fixture_union_name, + replace_paramfixture_with_values +): + @wraps(test_func, new_sig=new_sig) + async def wrapped_test_func(*args, **kwargs): # noqa + if kwargs.get(fixture_union_name, None) is NOT_USED: + # TODO why this ? it is probably useless: this fixture + # is private and will never end up in another union + return NOT_USED + else: + replace_paramfixture_with_values(kwargs) + return await test_func(*args, **kwargs) + + return wrapped_test_func diff --git a/src/pytest_cases/pep525.py b/src/pytest_cases/pep525.py new file mode 100644 index 00000000..17f002e8 --- /dev/null +++ b/src/pytest_cases/pep525.py @@ -0,0 +1,53 @@ +# Authors: Sylvain MARIE +# + All contributors to +# +# License: 3-clause BSD, + +# contains syntax illegal before PEP525 "Asynchronous Generators" + +from makefun import wraps +from .fixture_core1_unions import is_used_request, NOT_USED + + +def _ignore_unused_asyncgen_pep525(fixture_func, new_sig, func_needs_request): + @wraps(fixture_func, new_sig=new_sig) + async def wrapped_fixture_func(*args, **kwargs): + request = kwargs['request'] if func_needs_request else kwargs.pop('request') + if is_used_request(request): + async for res in fixture_func(*args, **kwargs): + yield res + else: + yield NOT_USED + + return wrapped_fixture_func + +def _decorate_fixture_plus_asyncgen_pep525(fixture_func, new_sig, map_arguments): + @wraps(fixture_func, new_sig=new_sig) + async def wrapped_fixture_func(*_args, **_kwargs): + if not is_used_request(_kwargs['request']): + yield NOT_USED + else: + _args, _kwargs = map_arguments(*_args, **_kwargs) + async for res in fixture_func(*_args, **_kwargs): + yield res + + return wrapped_fixture_func + +def _parametrize_plus_decorate_asyncgen_pep525( + test_func, + new_sig, + fixture_union_name, + replace_paramfixture_with_values +): + @wraps(test_func, new_sig=new_sig) + async def wrapped_test_func(*args, **kwargs): # noqa + if kwargs.get(fixture_union_name, None) is NOT_USED: + # TODO why this ? it is probably useless: this fixture + # is private and will never end up in another union + yield NOT_USED + else: + replace_paramfixture_with_values(kwargs) + async for res in test_func(*args, **kwargs): + yield res + + return wrapped_test_func diff --git a/tests/cases/issues/test_issue_286.py b/tests/cases/issues/test_issue_286.py new file mode 100644 index 00000000..2ebb6b37 --- /dev/null +++ b/tests/cases/issues/test_issue_286.py @@ -0,0 +1,19 @@ +import pytest +from inspect import isgeneratorfunction +from pytest_cases import fixture, parametrize + +def test_isgeneratorfunction_fixture(): + @fixture + def dummy(): + yield + + assert isgeneratorfunction(dummy.__pytest_wrapped__.obj) + + +# this covers parametrize aswell as parametrize_with_cases +def test_isgeneratorfunction_parametrize(): + @parametrize("a", [0, 1]) + def dummy(a): + yield + + assert isgeneratorfunction(dummy) diff --git a/tests/cases/issues/test_py35_issue_286.py b/tests/cases/issues/test_py35_issue_286.py new file mode 100644 index 00000000..0bca57bb --- /dev/null +++ b/tests/cases/issues/test_py35_issue_286.py @@ -0,0 +1,19 @@ +import pytest +from inspect import iscoroutinefunction +from pytest_cases import fixture, parametrize + +def test_iscoroutinefunction_fixture(): + @fixture + async def dummy(): + return + + assert iscoroutinefunction(dummy.__pytest_wrapped__.obj) + + +# this covers parametrize aswell as parametrize_with_cases +def test_iscoroutinefunction_parametrize(): + @parametrize("a", [0, 1]) + async def dummy(a): + return + + assert iscoroutinefunction(dummy) diff --git a/tests/cases/issues/test_py36_issue_286.py b/tests/cases/issues/test_py36_issue_286.py new file mode 100644 index 00000000..2b35a90b --- /dev/null +++ b/tests/cases/issues/test_py36_issue_286.py @@ -0,0 +1,19 @@ +import pytest +from inspect import isasyncgenfunction +from pytest_cases import fixture, parametrize + +def test_isasyncgenfunction_fixture(): + @fixture + async def dummy(): + yield + + assert isasyncgenfunction(dummy.__pytest_wrapped__.obj) + + +# this covers parametrize aswell as parametrize_with_cases +def test_isasyncgenfunction_parametrize(): + @parametrize("a", [0, 1]) + async def dummy(a): + yield + + assert isasyncgenfunction(dummy) diff --git a/tests/conftest.py b/tests/conftest.py index 8884669b..db0ac432 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,13 +31,17 @@ def pytest_ignore_collect(path, config): :param config: :return: """ + ignore_globs = [] + + if sys.version_info < (3, 6): + ignore_globs += ['**/*py36*.py'] if sys.version_info < (3, 5): - ignore_globs = ['**/*py35*.py'] - if any( - fnmatch.fnmatch(six.text_type(path), six.text_type(glob)) - for glob in ignore_globs - ): - return True + ignore_globs += ['**/*py35*.py'] + if any( + fnmatch.fnmatch(six.text_type(path), six.text_type(glob)) + for glob in ignore_globs + ): + return True # @pytest.hookimpl(trylast=True)