Skip to content

Commit 65ef48a

Browse files
agronholmgraingert
andauthored
Rebind instance method fixtures to the same instance as the test (#807)
Fixes #633. --------- Co-authored-by: Thomas Grainger <[email protected]>
1 parent a8f044b commit 65ef48a

File tree

3 files changed

+114
-9
lines changed

3 files changed

+114
-9
lines changed

docs/versionhistory.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
77

88
- Fixed acquring a lock twice in the same task on asyncio hanging instead of raising a
99
``RuntimeError`` (`#798 <https://github.com/agronholm/anyio/issues/798>`_)
10+
- Fixed an async fixture's ``self`` being different than the test's ``self`` in
11+
class-based tests (`#633 <https://github.com/agronholm/anyio/issues/633>`_)
12+
(PR by @agronholm and @graingert)
1013

1114
**4.6.0**
1215

src/anyio/pytest_plugin.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

33
import sys
4-
from collections.abc import Iterator
4+
from collections.abc import Generator, Iterator
55
from contextlib import ExitStack, contextmanager
6-
from inspect import isasyncgenfunction, iscoroutinefunction
6+
from inspect import isasyncgenfunction, iscoroutinefunction, ismethod
77
from typing import Any, cast
88

99
import pytest
1010
import sniffio
11+
from _pytest.fixtures import SubRequest
1112
from _pytest.outcomes import Exit
1213

1314
from ._core._eventloop import get_all_backends, get_async_backend
@@ -70,28 +71,56 @@ def pytest_configure(config: Any) -> None:
7071
)
7172

7273

73-
def pytest_fixture_setup(fixturedef: Any, request: Any) -> None:
74-
def wrapper(*args, anyio_backend, **kwargs): # type: ignore[no-untyped-def]
74+
@pytest.hookimpl(hookwrapper=True)
75+
def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]:
76+
def wrapper(
77+
*args: Any, anyio_backend: Any, request: SubRequest, **kwargs: Any
78+
) -> Any:
79+
# Rebind any fixture methods to the request instance
80+
if (
81+
request.instance
82+
and ismethod(func)
83+
and type(func.__self__) is type(request.instance)
84+
):
85+
local_func = func.__func__.__get__(request.instance)
86+
else:
87+
local_func = func
88+
7589
backend_name, backend_options = extract_backend_and_options(anyio_backend)
7690
if has_backend_arg:
7791
kwargs["anyio_backend"] = anyio_backend
7892

93+
if has_request_arg:
94+
kwargs["request"] = anyio_backend
95+
7996
with get_runner(backend_name, backend_options) as runner:
80-
if isasyncgenfunction(func):
81-
yield from runner.run_asyncgen_fixture(func, kwargs)
97+
if isasyncgenfunction(local_func):
98+
yield from runner.run_asyncgen_fixture(local_func, kwargs)
8299
else:
83-
yield runner.run_fixture(func, kwargs)
100+
yield runner.run_fixture(local_func, kwargs)
84101

85102
# Only apply this to coroutine functions and async generator functions in requests
86103
# that involve the anyio_backend fixture
87104
func = fixturedef.func
88105
if isasyncgenfunction(func) or iscoroutinefunction(func):
89106
if "anyio_backend" in request.fixturenames:
90-
has_backend_arg = "anyio_backend" in fixturedef.argnames
91107
fixturedef.func = wrapper
92-
if not has_backend_arg:
108+
original_argname = fixturedef.argnames
109+
110+
if not (has_backend_arg := "anyio_backend" in fixturedef.argnames):
93111
fixturedef.argnames += ("anyio_backend",)
94112

113+
if not (has_request_arg := "request" in fixturedef.argnames):
114+
fixturedef.argnames += ("request",)
115+
116+
try:
117+
return (yield)
118+
finally:
119+
fixturedef.func = func
120+
fixturedef.argnames = original_argname
121+
122+
return (yield)
123+
95124

96125
@pytest.hookimpl(tryfirst=True)
97126
def pytest_pycollect_makeitem(collector: Any, name: Any, obj: Any) -> None:

tests/test_pytest_plugin.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,76 @@ async def test_anyio_mark_first():
468468
)
469469

470470
testdir.runpytest_subprocess(*pytest_args, timeout=3)
471+
472+
473+
def test_async_fixture_in_test_class(testdir: Pytester) -> None:
474+
# Regression test for #633
475+
testdir.makepyfile(
476+
"""
477+
import pytest
478+
479+
480+
class TestAsyncFixtureMethod:
481+
is_same_instance = False
482+
483+
@pytest.fixture(autouse=True)
484+
async def async_fixture_method(self):
485+
self.is_same_instance = True
486+
487+
@pytest.mark.anyio
488+
async def test_async_fixture_method(self):
489+
assert self.is_same_instance
490+
"""
491+
)
492+
493+
result = testdir.runpytest_subprocess(*pytest_args)
494+
result.assert_outcomes(passed=len(get_all_backends()))
495+
496+
497+
def test_asyncgen_fixture_in_test_class(testdir: Pytester) -> None:
498+
# Regression test for #633
499+
testdir.makepyfile(
500+
"""
501+
import pytest
502+
503+
504+
class TestAsyncFixtureMethod:
505+
is_same_instance = False
506+
507+
@pytest.fixture(autouse=True)
508+
async def async_fixture_method(self):
509+
self.is_same_instance = True
510+
yield
511+
512+
@pytest.mark.anyio
513+
async def test_async_fixture_method(self):
514+
assert self.is_same_instance
515+
"""
516+
)
517+
518+
result = testdir.runpytest_subprocess(*pytest_args)
519+
result.assert_outcomes(passed=len(get_all_backends()))
520+
521+
522+
def test_anyio_fixture_adoption_does_not_persist(testdir: Pytester) -> None:
523+
testdir.makepyfile(
524+
"""
525+
import inspect
526+
import pytest
527+
528+
@pytest.fixture
529+
async def fixt():
530+
return 1
531+
532+
@pytest.mark.anyio
533+
async def test_fixt(fixt):
534+
assert fixt == 1
535+
536+
def test_no_mark(fixt):
537+
assert inspect.iscoroutine(fixt)
538+
fixt.close()
539+
"""
540+
)
541+
542+
result = testdir.runpytest(*pytest_args)
543+
result.assert_outcomes(passed=len(get_all_backends()) + 1)

0 commit comments

Comments
 (0)