From 618808958445806792e570eb5f6b5b32525d9375 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Sun, 6 Nov 2022 22:31:42 +0000 Subject: [PATCH] Handle bound fixture methods correctly 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. --- CHANGELOG.rst | 1 + pytest_asyncio/plugin.py | 59 ++++++++++++++----- tests/async_fixtures/test_async_fixtures.py | 12 ++++ .../async_fixtures/test_async_gen_fixtures.py | 13 ++++ 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 34fe9c08..34e8620b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `_ - Replaced usage of deprecated ``@pytest.mark.tryfirst`` with ``@pytest.hookimpl(tryfirst=True)`` `#438 `_ 0.20.1 (22-10-21) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 17268c9b..09937be8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -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( @@ -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__) + 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) + 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 @@ -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() diff --git a/tests/async_fixtures/test_async_fixtures.py b/tests/async_fixtures/test_async_fixtures.py index 7ddd04ab..40012962 100644 --- a/tests/async_fixtures/test_async_fixtures.py +++ b/tests/async_fixtures/test_async_fixtures.py @@ -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 diff --git a/tests/async_fixtures/test_async_gen_fixtures.py b/tests/async_fixtures/test_async_gen_fixtures.py index 0bea7458..2b198f2b 100644 --- a/tests/async_fixtures/test_async_gen_fixtures.py +++ b/tests/async_fixtures/test_async_gen_fixtures.py @@ -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