Skip to content

Commit d1c8c55

Browse files
authored
Make @fixture and @parametrize async aware (#301)
* Update makefun version * implement proper forwarding of async * rename new files * make parametrize async aware * Add tests * Add pytest ignore_glob for python < 3.6
1 parent 65c1bed commit d1c8c55

File tree

11 files changed

+332
-46
lines changed

11 files changed

+332
-46
lines changed

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ setup_requires =
3737
pytest-runner
3838
install_requires =
3939
decopatch
40-
makefun>=1.9.5
40+
makefun>=1.15.1
4141
packaging
4242
# note: pytest, too :)
4343
functools32;python_version<'3.2'

src/pytest_cases/fixture_core1_unions.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,26 @@
1010
from makefun import with_signature, add_signature_parameters, wraps
1111

1212
import pytest
13+
import sys
1314

1415
try: # python 3.3+
1516
from inspect import signature, Parameter
1617
except ImportError:
1718
from funcsigs import signature, Parameter # noqa
1819

20+
try: # native coroutines, python 3.5+
21+
from inspect import iscoroutinefunction
22+
except ImportError:
23+
def iscoroutinefunction(obj):
24+
return False
25+
26+
try: # native async generators, python 3.6+
27+
from inspect import isasyncgenfunction
28+
except ImportError:
29+
def isasyncgenfunction(obj):
30+
return False
31+
32+
1933
try: # type hints, python 3+
2034
from typing import Callable, Union, Optional, Any, List, Iterable, Sequence # noqa
2135
from types import ModuleType # noqa
@@ -224,7 +238,27 @@ def ignore_unused(fixture_func):
224238
else:
225239
new_sig = old_sig
226240

227-
if not isgeneratorfunction(fixture_func):
241+
if isasyncgenfunction(fixture_func) and sys.version_info >= (3, 6):
242+
from .pep525 import _ignore_unused_asyncgen_pep525
243+
wrapped_fixture_func = _ignore_unused_asyncgen_pep525(fixture_func, new_sig, func_needs_request)
244+
elif iscoroutinefunction(fixture_func) and sys.version_info >= (3, 5):
245+
from .pep492 import _ignore_unused_coroutine_pep492
246+
wrapped_fixture_func = _ignore_unused_coroutine_pep492(fixture_func, new_sig, func_needs_request)
247+
elif isgeneratorfunction(fixture_func):
248+
if sys.version_info >= (3, 3):
249+
from .pep380 import _ignore_unused_generator_pep380
250+
wrapped_fixture_func = _ignore_unused_generator_pep380(fixture_func, new_sig, func_needs_request)
251+
else:
252+
# generator function (with a yield statement)
253+
@wraps(fixture_func, new_sig=new_sig)
254+
def wrapped_fixture_func(*args, **kwargs):
255+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
256+
if is_used_request(request):
257+
for res in fixture_func(*args, **kwargs):
258+
yield res
259+
else:
260+
yield NOT_USED
261+
else:
228262
# normal function with return statement
229263
@wraps(fixture_func, new_sig=new_sig)
230264
def wrapped_fixture_func(*args, **kwargs):
@@ -234,17 +268,6 @@ def wrapped_fixture_func(*args, **kwargs):
234268
else:
235269
return NOT_USED
236270

237-
else:
238-
# generator function (with a yield statement)
239-
@wraps(fixture_func, new_sig=new_sig)
240-
def wrapped_fixture_func(*args, **kwargs):
241-
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
242-
if is_used_request(request):
243-
for res in fixture_func(*args, **kwargs):
244-
yield res
245-
else:
246-
yield NOT_USED
247-
248271
return wrapped_fixture_func
249272

250273

src/pytest_cases/fixture_core2.py

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,25 @@
1212
from makefun import with_signature, add_signature_parameters, remove_signature_parameters, wraps
1313

1414
import pytest
15+
import sys
1516

1617
try: # python 3.3+
1718
from inspect import signature, Parameter
1819
except ImportError:
1920
from funcsigs import signature, Parameter # noqa
2021

22+
try: # native coroutines, python 3.5+
23+
from inspect import iscoroutinefunction
24+
except ImportError:
25+
def iscoroutinefunction(obj):
26+
return False
27+
28+
try: # native async generators, python 3.6+
29+
from inspect import isasyncgenfunction
30+
except ImportError:
31+
def isasyncgenfunction(obj):
32+
return False
33+
2134
try: # type hints, python 3+
2235
from typing import Callable, Union, Any, List, Iterable, Sequence # noqa
2336
from types import ModuleType # noqa
@@ -528,7 +541,27 @@ def _map_arguments(*_args, **_kwargs):
528541
return _args, _kwargs
529542

530543
# --Finally create the fixture function, a wrapper of user-provided fixture with the new signature
531-
if not isgeneratorfunction(fixture_func):
544+
if isasyncgenfunction(fixture_func)and sys.version_info >= (3, 6):
545+
from .pep525 import _decorate_fixture_plus_asyncgen_pep525
546+
wrapped_fixture_func = _decorate_fixture_plus_asyncgen_pep525(fixture_func, new_sig, _map_arguments)
547+
elif iscoroutinefunction(fixture_func) and sys.version_info >= (3, 5):
548+
from .pep492 import _decorate_fixture_plus_coroutine_pep492
549+
wrapped_fixture_func = _decorate_fixture_plus_coroutine_pep492(fixture_func, new_sig, _map_arguments)
550+
elif isgeneratorfunction(fixture_func):
551+
# generator function (with a yield statement)
552+
if sys.version_info >= (3, 3):
553+
from .pep380 import _decorate_fixture_plus_generator_pep380
554+
wrapped_fixture_func = _decorate_fixture_plus_generator_pep380(fixture_func, new_sig, _map_arguments)
555+
else:
556+
@wraps(fixture_func, new_sig=new_sig)
557+
def wrapped_fixture_func(*_args, **_kwargs):
558+
if not is_used_request(_kwargs['request']):
559+
yield NOT_USED
560+
else:
561+
_args, _kwargs = _map_arguments(*_args, **_kwargs)
562+
for res in fixture_func(*_args, **_kwargs):
563+
yield res
564+
else:
532565
# normal function with return statement
533566
@wraps(fixture_func, new_sig=new_sig)
534567
def wrapped_fixture_func(*_args, **_kwargs):
@@ -538,17 +571,6 @@ def wrapped_fixture_func(*_args, **_kwargs):
538571
_args, _kwargs = _map_arguments(*_args, **_kwargs)
539572
return fixture_func(*_args, **_kwargs)
540573

541-
else:
542-
# generator function (with a yield statement)
543-
@wraps(fixture_func, new_sig=new_sig)
544-
def wrapped_fixture_func(*_args, **_kwargs):
545-
if not is_used_request(_kwargs['request']):
546-
yield NOT_USED
547-
else:
548-
_args, _kwargs = _map_arguments(*_args, **_kwargs)
549-
for res in fixture_func(*_args, **_kwargs):
550-
yield res
551-
552574
# transform the created wrapper into a fixture
553575
_make_fix = pytest_fixture(scope=scope, params=final_values, autouse=autouse, hook=hook, ids=final_ids, **kwargs)
554576
return _make_fix(wrapped_fixture_func)

src/pytest_cases/fixture_parametrize_plus.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111
except ImportError:
1212
from funcsigs import signature, Parameter # noqa
1313

14+
try: # native coroutines, python 3.5+
15+
from inspect import iscoroutinefunction
16+
except ImportError:
17+
def iscoroutinefunction(obj):
18+
return False
19+
20+
try: # native async generators, python 3.6+
21+
from inspect import isasyncgenfunction
22+
except ImportError:
23+
def isasyncgenfunction(obj):
24+
return False
25+
1426
try:
1527
from collections.abc import Iterable
1628
except ImportError: # noqa
@@ -25,6 +37,7 @@
2537
pass
2638

2739
import pytest
40+
import sys
2841
from makefun import with_signature, remove_signature_parameters, add_signature_parameters, wraps
2942

3043
from .common_mini_six import string_types
@@ -1059,30 +1072,44 @@ def replace_paramfixture_with_values(kwargs): # noqa
10591072
# return
10601073
return kwargs
10611074

1062-
if not isgeneratorfunction(test_func):
1063-
# normal test or fixture function with return statement
1064-
@wraps(test_func, new_sig=new_sig)
1065-
def wrapped_test_func(*args, **kwargs): # noqa
1066-
if kwargs.get(fixture_union_name, None) is NOT_USED:
1067-
# TODO why this ? it is probably useless: this fixture
1068-
# is private and will never end up in another union
1069-
return NOT_USED
1070-
else:
1071-
replace_paramfixture_with_values(kwargs)
1072-
return test_func(*args, **kwargs)
10731075

1076+
if isasyncgenfunction(test_func)and sys.version_info >= (3, 6):
1077+
from .pep525 import _parametrize_plus_decorate_asyncgen_pep525
1078+
wrapped_test_func = _parametrize_plus_decorate_asyncgen_pep525(test_func, new_sig, fixture_union_name,
1079+
replace_paramfixture_with_values)
1080+
elif iscoroutinefunction(test_func) and sys.version_info >= (3, 5):
1081+
from .pep492 import _parametrize_plus_decorate_coroutine_pep492
1082+
wrapped_test_func = _parametrize_plus_decorate_coroutine_pep492(test_func, new_sig, fixture_union_name,
1083+
replace_paramfixture_with_values)
1084+
elif isgeneratorfunction(test_func):
1085+
# generator function (with a yield statement)
1086+
if sys.version_info >= (3, 3):
1087+
from .pep380 import _parametrize_plus_decorate_generator_pep380
1088+
wrapped_test_func = _parametrize_plus_decorate_generator_pep380(test_func, new_sig,
1089+
fixture_union_name,
1090+
replace_paramfixture_with_values)
1091+
else:
1092+
@wraps(test_func, new_sig=new_sig)
1093+
def wrapped_test_func(*args, **kwargs): # noqa
1094+
if kwargs.get(fixture_union_name, None) is NOT_USED:
1095+
# TODO why this ? it is probably useless: this fixture
1096+
# is private and will never end up in another union
1097+
yield NOT_USED
1098+
else:
1099+
replace_paramfixture_with_values(kwargs)
1100+
for res in test_func(*args, **kwargs):
1101+
yield res
10741102
else:
1075-
# generator test or fixture function (with one or several yield statements)
1103+
# normal function with return statement
10761104
@wraps(test_func, new_sig=new_sig)
10771105
def wrapped_test_func(*args, **kwargs): # noqa
10781106
if kwargs.get(fixture_union_name, None) is NOT_USED:
10791107
# TODO why this ? it is probably useless: this fixture
10801108
# is private and will never end up in another union
1081-
yield NOT_USED
1109+
return NOT_USED
10821110
else:
10831111
replace_paramfixture_with_values(kwargs)
1084-
for res in test_func(*args, **kwargs):
1085-
yield res
1112+
return test_func(*args, **kwargs)
10861113

10871114
# move all pytest marks from the test function to the wrapper
10881115
# not needed because the __dict__ is automatically copied when we use @wraps

src/pytest_cases/pep380.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Authors: Sylvain MARIE <[email protected]>
2+
# + All contributors to <https://github.com/smarie/python-pytest-cases>
3+
#
4+
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
6+
# contains syntax illegal before PEP380 'Syntax for Delegating to a Subgenerator'
7+
8+
from makefun import wraps
9+
from .fixture_core1_unions import is_used_request, NOT_USED
10+
11+
12+
def _ignore_unused_generator_pep380(fixture_func, new_sig, func_needs_request):
13+
@wraps(fixture_func, new_sig=new_sig)
14+
def wrapped_fixture_func(*args, **kwargs):
15+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
16+
if is_used_request(request):
17+
yield from fixture_func(*args, **kwargs)
18+
else:
19+
yield NOT_USED
20+
21+
return wrapped_fixture_func
22+
23+
def _decorate_fixture_plus_generator_pep380(fixture_func, new_sig, map_arguments):
24+
@wraps(fixture_func, new_sig=new_sig)
25+
def wrapped_fixture_func(*_args, **_kwargs):
26+
if not is_used_request(_kwargs['request']):
27+
yield NOT_USED
28+
else:
29+
_args, _kwargs = map_arguments(*_args, **_kwargs)
30+
yield from fixture_func(*_args, **_kwargs)
31+
32+
return wrapped_fixture_func
33+
34+
def _parametrize_plus_decorate_generator_pep380(
35+
test_func,
36+
new_sig,
37+
fixture_union_name,
38+
replace_paramfixture_with_values
39+
):
40+
@wraps(test_func, new_sig=new_sig)
41+
def wrapped_test_func(*args, **kwargs): # noqa
42+
if kwargs.get(fixture_union_name, None) is NOT_USED:
43+
# TODO why this ? it is probably useless: this fixture
44+
# is private and will never end up in another union
45+
yield NOT_USED
46+
else:
47+
replace_paramfixture_with_values(kwargs)
48+
yield from test_func(*args, **kwargs)
49+
50+
return wrapped_test_func

src/pytest_cases/pep492.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Authors: Sylvain MARIE <[email protected]>
2+
# + All contributors to <https://github.com/smarie/python-pytest-cases>
3+
#
4+
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
6+
# contains syntax illegal before PEP492 "Coroutines with async and await syntax"
7+
8+
from makefun import wraps
9+
from .fixture_core1_unions import is_used_request, NOT_USED
10+
11+
12+
def _ignore_unused_coroutine_pep492(fixture_func, new_sig, func_needs_request):
13+
@wraps(fixture_func, new_sig=new_sig)
14+
async def wrapped_fixture_func(*args, **kwargs):
15+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
16+
if is_used_request(request):
17+
return await fixture_func(*args, **kwargs)
18+
else:
19+
return NOT_USED
20+
21+
return wrapped_fixture_func
22+
23+
def _decorate_fixture_plus_coroutine_pep492(fixture_func, new_sig, map_arguments):
24+
@wraps(fixture_func, new_sig=new_sig)
25+
async def wrapped_fixture_func(*_args, **_kwargs):
26+
if not is_used_request(_kwargs['request']):
27+
return NOT_USED
28+
else:
29+
_args, _kwargs = map_arguments(*_args, **_kwargs)
30+
return await fixture_func(*_args, **_kwargs)
31+
32+
return wrapped_fixture_func
33+
34+
def _parametrize_plus_decorate_coroutine_pep492(
35+
test_func,
36+
new_sig,
37+
fixture_union_name,
38+
replace_paramfixture_with_values
39+
):
40+
@wraps(test_func, new_sig=new_sig)
41+
async def wrapped_test_func(*args, **kwargs): # noqa
42+
if kwargs.get(fixture_union_name, None) is NOT_USED:
43+
# TODO why this ? it is probably useless: this fixture
44+
# is private and will never end up in another union
45+
return NOT_USED
46+
else:
47+
replace_paramfixture_with_values(kwargs)
48+
return await test_func(*args, **kwargs)
49+
50+
return wrapped_test_func

src/pytest_cases/pep525.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Authors: Sylvain MARIE <[email protected]>
2+
# + All contributors to <https://github.com/smarie/python-pytest-cases>
3+
#
4+
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
6+
# contains syntax illegal before PEP525 "Asynchronous Generators"
7+
8+
from makefun import wraps
9+
from .fixture_core1_unions import is_used_request, NOT_USED
10+
11+
12+
def _ignore_unused_asyncgen_pep525(fixture_func, new_sig, func_needs_request):
13+
@wraps(fixture_func, new_sig=new_sig)
14+
async def wrapped_fixture_func(*args, **kwargs):
15+
request = kwargs['request'] if func_needs_request else kwargs.pop('request')
16+
if is_used_request(request):
17+
async for res in fixture_func(*args, **kwargs):
18+
yield res
19+
else:
20+
yield NOT_USED
21+
22+
return wrapped_fixture_func
23+
24+
def _decorate_fixture_plus_asyncgen_pep525(fixture_func, new_sig, map_arguments):
25+
@wraps(fixture_func, new_sig=new_sig)
26+
async def wrapped_fixture_func(*_args, **_kwargs):
27+
if not is_used_request(_kwargs['request']):
28+
yield NOT_USED
29+
else:
30+
_args, _kwargs = map_arguments(*_args, **_kwargs)
31+
async for res in fixture_func(*_args, **_kwargs):
32+
yield res
33+
34+
return wrapped_fixture_func
35+
36+
def _parametrize_plus_decorate_asyncgen_pep525(
37+
test_func,
38+
new_sig,
39+
fixture_union_name,
40+
replace_paramfixture_with_values
41+
):
42+
@wraps(test_func, new_sig=new_sig)
43+
async def wrapped_test_func(*args, **kwargs): # noqa
44+
if kwargs.get(fixture_union_name, None) is NOT_USED:
45+
# TODO why this ? it is probably useless: this fixture
46+
# is private and will never end up in another union
47+
yield NOT_USED
48+
else:
49+
replace_paramfixture_with_values(kwargs)
50+
async for res in test_func(*args, **kwargs):
51+
yield res
52+
53+
return wrapped_test_func

0 commit comments

Comments
 (0)