diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index c71002b8..3a0d2e0d 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning 2.0 `_.
- Fixed acquring a lock twice in the same task on asyncio hanging instead of raising a
``RuntimeError`` (`#798 `_)
+- Fixed an async fixture's ``self`` being different than the test's ``self`` in
+ class-based tests (`#633 `_)
+ (PR by @agronholm and @graingert)
**4.6.0**
diff --git a/src/anyio/pytest_plugin.py b/src/anyio/pytest_plugin.py
index c9fe1bde..0c42ab71 100644
--- a/src/anyio/pytest_plugin.py
+++ b/src/anyio/pytest_plugin.py
@@ -1,13 +1,14 @@
from __future__ import annotations
import sys
-from collections.abc import Iterator
+from collections.abc import Generator, Iterator
from contextlib import ExitStack, contextmanager
-from inspect import isasyncgenfunction, iscoroutinefunction
+from inspect import isasyncgenfunction, iscoroutinefunction, ismethod
from typing import Any, cast
import pytest
import sniffio
+from _pytest.fixtures import SubRequest
from _pytest.outcomes import Exit
from ._core._eventloop import get_all_backends, get_async_backend
@@ -70,28 +71,56 @@ def pytest_configure(config: Any) -> None:
)
-def pytest_fixture_setup(fixturedef: Any, request: Any) -> None:
- def wrapper(*args, anyio_backend, **kwargs): # type: ignore[no-untyped-def]
+@pytest.hookimpl(hookwrapper=True)
+def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]:
+ def wrapper(
+ *args: Any, anyio_backend: Any, request: SubRequest, **kwargs: Any
+ ) -> Any:
+ # Rebind any fixture methods to the request instance
+ if (
+ request.instance
+ and ismethod(func)
+ and type(func.__self__) is type(request.instance)
+ ):
+ local_func = func.__func__.__get__(request.instance)
+ else:
+ local_func = func
+
backend_name, backend_options = extract_backend_and_options(anyio_backend)
if has_backend_arg:
kwargs["anyio_backend"] = anyio_backend
+ if has_request_arg:
+ kwargs["request"] = anyio_backend
+
with get_runner(backend_name, backend_options) as runner:
- if isasyncgenfunction(func):
- yield from runner.run_asyncgen_fixture(func, kwargs)
+ if isasyncgenfunction(local_func):
+ yield from runner.run_asyncgen_fixture(local_func, kwargs)
else:
- yield runner.run_fixture(func, kwargs)
+ yield runner.run_fixture(local_func, kwargs)
# Only apply this to coroutine functions and async generator functions in requests
# that involve the anyio_backend fixture
func = fixturedef.func
if isasyncgenfunction(func) or iscoroutinefunction(func):
if "anyio_backend" in request.fixturenames:
- has_backend_arg = "anyio_backend" in fixturedef.argnames
fixturedef.func = wrapper
- if not has_backend_arg:
+ original_argname = fixturedef.argnames
+
+ if not (has_backend_arg := "anyio_backend" in fixturedef.argnames):
fixturedef.argnames += ("anyio_backend",)
+ if not (has_request_arg := "request" in fixturedef.argnames):
+ fixturedef.argnames += ("request",)
+
+ try:
+ return (yield)
+ finally:
+ fixturedef.func = func
+ fixturedef.argnames = original_argname
+
+ return (yield)
+
@pytest.hookimpl(tryfirst=True)
def pytest_pycollect_makeitem(collector: Any, name: Any, obj: Any) -> None:
diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py
index 1aa8911e..f1b20f91 100644
--- a/tests/test_pytest_plugin.py
+++ b/tests/test_pytest_plugin.py
@@ -468,3 +468,76 @@ async def test_anyio_mark_first():
)
testdir.runpytest_subprocess(*pytest_args, timeout=3)
+
+
+def test_async_fixture_in_test_class(testdir: Pytester) -> None:
+ # Regression test for #633
+ testdir.makepyfile(
+ """
+ import pytest
+
+
+ class TestAsyncFixtureMethod:
+ is_same_instance = False
+
+ @pytest.fixture(autouse=True)
+ async def async_fixture_method(self):
+ self.is_same_instance = True
+
+ @pytest.mark.anyio
+ async def test_async_fixture_method(self):
+ assert self.is_same_instance
+ """
+ )
+
+ result = testdir.runpytest_subprocess(*pytest_args)
+ result.assert_outcomes(passed=len(get_all_backends()))
+
+
+def test_asyncgen_fixture_in_test_class(testdir: Pytester) -> None:
+ # Regression test for #633
+ testdir.makepyfile(
+ """
+ import pytest
+
+
+ class TestAsyncFixtureMethod:
+ is_same_instance = False
+
+ @pytest.fixture(autouse=True)
+ async def async_fixture_method(self):
+ self.is_same_instance = True
+ yield
+
+ @pytest.mark.anyio
+ async def test_async_fixture_method(self):
+ assert self.is_same_instance
+ """
+ )
+
+ result = testdir.runpytest_subprocess(*pytest_args)
+ result.assert_outcomes(passed=len(get_all_backends()))
+
+
+def test_anyio_fixture_adoption_does_not_persist(testdir: Pytester) -> None:
+ testdir.makepyfile(
+ """
+ import inspect
+ import pytest
+
+ @pytest.fixture
+ async def fixt():
+ return 1
+
+ @pytest.mark.anyio
+ async def test_fixt(fixt):
+ assert fixt == 1
+
+ def test_no_mark(fixt):
+ assert inspect.iscoroutine(fixt)
+ fixt.close()
+ """
+ )
+
+ result = testdir.runpytest(*pytest_args)
+ result.assert_outcomes(passed=len(get_all_backends()) + 1)