Skip to content

Commit

Permalink
Make @fixture and @parametrize async aware (#301)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jgersti committed Oct 11, 2023
1 parent 65c1bed commit d1c8c55
Show file tree
Hide file tree
Showing 11 changed files with 332 additions and 46 deletions.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
47 changes: 35 additions & 12 deletions src/pytest_cases/fixture_core1_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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


Expand Down
46 changes: 34 additions & 12 deletions src/pytest_cases/fixture_core2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
57 changes: 42 additions & 15 deletions src/pytest_cases/fixture_parametrize_plus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions src/pytest_cases/pep380.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Authors: Sylvain MARIE <[email protected]>
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>

# 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
50 changes: 50 additions & 0 deletions src/pytest_cases/pep492.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Authors: Sylvain MARIE <[email protected]>
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>

# 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
53 changes: 53 additions & 0 deletions src/pytest_cases/pep525.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Authors: Sylvain MARIE <[email protected]>
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>

# 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
Loading

0 comments on commit d1c8c55

Please sign in to comment.