Skip to content

Commit fe63e34

Browse files
authored
Handle bound fixture methods correctly (#439)
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 38fc032 commit fe63e34

File tree

4 files changed

+70
-15
lines changed

4 files changed

+70
-15
lines changed

CHANGELOG.rst

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44

55
UNRELEASED
66
=================
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>`_
78
- Replaced usage of deprecated ``@pytest.mark.tryfirst`` with ``@pytest.hookimpl(tryfirst=True)`` `#438 <https://github.com/pytest-dev/pytest-asyncio/pull/438>`_
89

910
0.20.1 (22-10-21)

pytest_asyncio/plugin.py

+44-15
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__) # type: ignore
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) # type: ignore
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:
290-
async def setup() -> _R:
314+
):
315+
func = _perhaps_rebind_fixture_func(
316+
fixture, request.instance, fixturedef.unittest
317+
)
318+
319+
async def setup():
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

+12
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

+13
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)