Skip to content

Commit cef53c9

Browse files
committed
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.
1 parent 28ba705 commit cef53c9

File tree

4 files changed

+72
-14
lines changed

4 files changed

+72
-14
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
Changelog
33
=========
44

5+
UNRELEASED
6+
=================
7+
- 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>`_
8+
59
0.20.1 (22-10-21)
610
=================
711
- Fixes an issue that warned about using an old version of pytest, even though the most recent version was installed. `#430 <https://github.com/pytest-dev/pytest-asyncio/issues/430>`_

pytest_asyncio/plugin.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,10 @@ def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
227227
"""
228228
Wraps the fixture function of an async fixture in a synchronous function.
229229
"""
230-
func = fixturedef.func
231-
if inspect.isasyncgenfunction(func):
232-
fixturedef.func = _wrap_asyncgen(func)
233-
elif inspect.iscoroutinefunction(func):
234-
fixturedef.func = _wrap_async(func)
230+
if inspect.isasyncgenfunction(fixturedef.func):
231+
_wrap_asyncgen_fixture(fixturedef)
232+
elif inspect.iscoroutinefunction(fixturedef.func):
233+
_wrap_async_fixture(fixturedef)
235234

236235

237236
def _add_kwargs(
@@ -249,14 +248,38 @@ def _add_kwargs(
249248
return ret
250249

251250

252-
def _wrap_asyncgen(func: Callable[..., AsyncIterator[_R]]) -> Callable[..., _R]:
253-
@functools.wraps(func)
251+
def _perhaps_rebind_fixture_func(
252+
func: _T, instance: Optional[Any], unittest: bool
253+
) -> _T:
254+
if instance is not None:
255+
# The fixture needs to be bound to the actual request.instance
256+
# so it is bound to the same object as the test method.
257+
unbound, cls = func, None
258+
try:
259+
unbound, cls = func.__func__, type(func.__self__)
260+
except AttributeError:
261+
pass
262+
# If unittest is true, the fixture is bound unconditionally.
263+
# otherwise, only if the fixture was bound before to an instance of
264+
# the same type.
265+
if unittest or (cls is not None and isinstance(instance, cls)):
266+
func = unbound.__get__(instance)
267+
return func
268+
269+
270+
def _wrap_asyncgen_fixture(fixturedef: FixtureDef) -> None:
271+
fixture = fixturedef.func
272+
273+
@functools.wraps(fixture)
254274
def _asyncgen_fixture_wrapper(
255275
event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
256-
) -> _R:
276+
):
277+
func = _perhaps_rebind_fixture_func(
278+
fixture, request.instance, fixturedef.unittest
279+
)
257280
gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request))
258281

259-
async def setup() -> _R:
282+
async def setup():
260283
res = await gen_obj.__anext__()
261284
return res
262285

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

282-
return _asyncgen_fixture_wrapper
305+
fixturedef.func = _asyncgen_fixture_wrapper
283306

284307

285-
def _wrap_async(func: Callable[..., Awaitable[_R]]) -> Callable[..., _R]:
286-
@functools.wraps(func)
308+
def _wrap_async_fixture(fixturedef: FixtureDef) -> None:
309+
fixture = fixturedef.func
310+
311+
@functools.wraps(fixture)
287312
def _async_fixture_wrapper(
288313
event_loop: asyncio.AbstractEventLoop, request: SubRequest, **kwargs: Any
289-
) -> _R:
314+
):
315+
func = _perhaps_rebind_fixture_func(
316+
fixture, request.instance, fixturedef.unittest
317+
)
318+
290319
async def setup() -> _R:
291320
res = await func(**_add_kwargs(func, kwargs, event_loop, request))
292321
return res
293322

294323
return event_loop.run_until_complete(setup())
295324

296-
return _async_fixture_wrapper
325+
fixturedef.func = _async_fixture_wrapper
297326

298327

299328
_HOLDER: Set[FixtureDef] = set()

tests/async_fixtures/test_async_fixtures.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ async def test_async_fixture(async_fixture, mock):
2323
assert mock.call_count == 1
2424
assert mock.call_args_list[-1] == unittest.mock.call(START)
2525
assert async_fixture is RETVAL
26+
27+
28+
class TestAsyncFixtureMethod:
29+
is_same_instance = False
30+
31+
@pytest.fixture(autouse=True)
32+
async def async_fixture_method(self):
33+
self.is_same_instance = True
34+
35+
@pytest.mark.asyncio
36+
async def test_async_fixture_method(self):
37+
assert self.is_same_instance

tests/async_fixtures/test_async_gen_fixtures.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,16 @@ async def test_async_gen_fixture_finalized(mock):
3636
assert mock.call_args_list[-1] == unittest.mock.call(END)
3737
finally:
3838
mock.reset_mock()
39+
40+
41+
class TestAsyncGenFixtureMethod:
42+
is_same_instance = False
43+
44+
@pytest.fixture(autouse=True)
45+
async def async_gen_fixture_method(self):
46+
self.is_same_instance = True
47+
yield None
48+
49+
@pytest.mark.asyncio
50+
async def test_async_gen_fixture_method(self):
51+
assert self.is_same_instance

0 commit comments

Comments
 (0)