From e69738ac7f9e7c8cbaca1a6419a474da37151648 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 | 4 ++ pytest_asyncio/plugin.py | 59 ++++++++++++++----- tests/async_fixtures/test_async_fixtures.py | 12 ++++ .../async_fixtures/test_async_gen_fixtures.py | 13 ++++ 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5c84b46e..94b5f1a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ 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 `_ + 0.20.1 (22-10-21) ================= - Fixes an issue that warned about using an old version of pytest, even though the most recent version was installed. `#430 `_ diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 3b7b2304..3bf23185 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