Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle bound fixture methods correctly #439

Merged
merged 1 commit into from Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Expand Up @@ -4,6 +4,7 @@ Changelog

UNRELEASED
=================
- Fixes an issue with async fixtures that are defined as methods on a test class not being rebound to the actual test instance. `#197 <https://github.com/pytest-dev/pytest-asyncio/issues/197>`_
- Replaced usage of deprecated ``@pytest.mark.tryfirst`` with ``@pytest.hookimpl(tryfirst=True)`` `#438 <https://github.com/pytest-dev/pytest-asyncio/pull/438>`_

0.20.1 (22-10-21)
Expand Down
59 changes: 44 additions & 15 deletions pytest_asyncio/plugin.py
Expand Up @@ -227,11 +227,10 @@ def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
"""
Wraps the fixture function of an async fixture in a synchronous function.
"""
func = fixturedef.func
if inspect.isasyncgenfunction(func):
fixturedef.func = _wrap_asyncgen(func)
elif inspect.iscoroutinefunction(func):
fixturedef.func = _wrap_async(func)
if inspect.isasyncgenfunction(fixturedef.func):
_wrap_asyncgen_fixture(fixturedef)
elif inspect.iscoroutinefunction(fixturedef.func):
_wrap_async_fixture(fixturedef)


def _add_kwargs(
Expand All @@ -249,14 +248,38 @@ def _add_kwargs(
return ret


def _wrap_asyncgen(func: Callable[..., AsyncIterator[_R]]) -> Callable[..., _R]:
@functools.wraps(func)
def _perhaps_rebind_fixture_func(
seifertm marked this conversation as resolved.
Show resolved Hide resolved
func: _T, instance: Optional[Any], unittest: bool
mjpieters marked this conversation as resolved.
Show resolved Hide resolved
) -> _T:
if instance is not None:
# The fixture needs to be bound to the actual request.instance
# so it is bound to the same object as the test method.
unbound, cls = func, None
try:
unbound, cls = func.__func__, type(func.__self__) # type: ignore
except AttributeError:
pass
# If unittest is true, the fixture is bound unconditionally.
# otherwise, only if the fixture was bound before to an instance of
# the same type.
if unittest or (cls is not None and isinstance(instance, cls)):
func = unbound.__get__(instance) # type: ignore
return func


def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
fixture = fixturedef.func

@functools.wraps(fixture)
def _asyncgen_fixture_wrapper(
event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
) -> _R:
):
func = _perhaps_rebind_fixture_func(
fixture, request.instance, fixturedef.unittest
)
gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))

async def setup() -> _R:
async def setup():
res = await gen_obj.__anext__()
return res

Expand All @@ -279,21 +302,27 @@ async def async_finalizer() -> None:
request.addfinalizer(finalizer)
return result

return _asyncgen_fixture_wrapper
fixturedef.func = _asyncgen_fixture_wrapper


def _wrap_async(func: Callable[..., Awaitable[_R]]) -> Callable[..., _R]:
@functools.wraps(func)
def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
fixture = fixturedef.func

@functools.wraps(fixture)
def _async_fixture_wrapper(
event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
) -> _R:
async def setup() -> _R:
):
func = _perhaps_rebind_fixture_func(
fixture, request.instance, fixturedef.unittest
)

async def setup():
res = await func(**_add_kwargs(func, kwargs, event_loop, request))
return res

return event_loop.run_until_complete(setup())

return _async_fixture_wrapper
fixturedef.func = _async_fixture_wrapper


_HOLDER: Set[FixtureDef] = set()
Expand Down
12 changes: 12 additions & 0 deletions tests/async_fixtures/test_async_fixtures.py
Expand Up @@ -23,3 +23,15 @@ async def test_async_fixture(async_fixture, mock):
assert mock.call_count == 1
assert mock.call_args_list[-1] == unittest.mock.call(START)
assert async_fixture is RETVAL


class TestAsyncFixtureMethod:
is_same_instance = False

@pytest.fixture(autouse=True)
async def async_fixture_method(self):
self.is_same_instance = True

@pytest.mark.asyncio
async def test_async_fixture_method(self):
assert self.is_same_instance
13 changes: 13 additions & 0 deletions tests/async_fixtures/test_async_gen_fixtures.py
Expand Up @@ -36,3 +36,16 @@ async def test_async_gen_fixture_finalized(mock):
assert mock.call_args_list[-1] == unittest.mock.call(END)
finally:
mock.reset_mock()


class TestAsyncGenFixtureMethod:
is_same_instance = False

@pytest.fixture(autouse=True)
async def async_gen_fixture_method(self):
self.is_same_instance = True
yield None

@pytest.mark.asyncio
async def test_async_gen_fixture_method(self):
assert self.is_same_instance