Skip to content

Commit

Permalink
Rebind instance method fixtures to the same instance as the test (#807)
Browse files Browse the repository at this point in the history
Fixes #633.

---------

Co-authored-by: Thomas Grainger <tagrain@gmail.com>

(cherry picked from commit 65ef48a)
  • Loading branch information
agronholm committed Oct 13, 2024
1 parent 57bcbc9 commit e8546bd
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 9 deletions.
3 changes: 3 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

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

**4.6.0**

Expand Down
47 changes: 38 additions & 9 deletions src/anyio/pytest_plugin.py
Original file line number Diff line number Diff line change
@@ -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, Dict, Tuple, 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
Expand Down Expand Up @@ -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:
Expand Down
73 changes: 73 additions & 0 deletions tests/test_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit e8546bd

Please sign in to comment.