Skip to content

Handle bound fixture methods correctly #439

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

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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(
func: _T, instance: Optional[Any], unittest: bool
) -> _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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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