Skip to content

Commit

Permalink
Handle bound fixture methods correctly
Browse files Browse the repository at this point in the history
When the current test request references an instance, bind the fixture function to that instance. When the unittest flag is set, this happens unconditionally, otherwise only if:

- the fixture wasn't bound already
- the fixture is bound to a compatible instance (the request.instance object has the same type or is a subclass of that type).

This follows what pytest does in such cases, exactly.
  • Loading branch information
mjpieters committed Nov 10, 2022
1 parent 38fc032 commit 8860dc5
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 15 deletions.
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(
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
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

0 comments on commit 8860dc5

Please sign in to comment.