1 from contextlib import contextmanager

2 from inspect import isasyncgenfunction, iscoroutinefunction

3 from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Tuple, cast

4

5 import pytest

6 import sniffio

7 from _pytest.fixtures import FixtureRequest

8

9 from ._core._eventloop import get_all_backends, get_asynclib

10 from .abc import TestRunner

11

12 if TYPE_CHECKING:

13 from _pytest.config import Config

14

15_current_runner: Optional[TestRunner] = None

16

17

18 def extract_backend_and_options(backend: object) -> Tuple[str, Dict[str, Any]]:

19 if isinstance(backend, str):

20 return backend, {}

21 elif isinstance(backend, tuple) and len(backend) == 2:

22 if isinstance(backend[0], str) and isinstance(backend[1], dict):

23 return cast(Tuple[str, Dict[str, Any]], backend)

24

25 raise TypeError("anyio_backend must be either a string or tuple of (string, dict)")

26

27

28@contextmanager

29 def get_runner(

30 backend_name: str, backend_options: Dict[str, Any]

31) -> Generator[TestRunner, object, None]:

32 global _current_runner

33 if _current_runner:

34 yield _current_runner

35 return

36

37 asynclib = get_asynclib(backend_name)

38 token = None

39 if sniffio.current_async_library_cvar.get(None) is None:

40 # Since we're in control of the event loop, we can cache the name of the async library

41 token = sniffio.current_async_library_cvar.set(backend_name)

42

43 try:

44 backend_options = backend_options or {}

45 with asynclib.TestRunner(**backend_options) as runner:

46 _current_runner = runner

47 yield runner

48 finally:

49 _current_runner = None

50 if token:

51 sniffio.current_async_library_cvar.reset(token)

52

53

54 def pytest_configure(config: "Config") -> None:

55 config.addinivalue_line(

56 "markers",

57 "anyio: mark the (coroutine function) test to be run "

58 "asynchronously via anyio.",

59 )

60

61

62 def pytest_fixture_setup(fixturedef: Any, request: FixtureRequest) -> None:

63 def wrapper(*args, anyio_backend, **kwargs): # type: ignore[no-untyped-def]

64 backend_name, backend_options = extract_backend_and_options(anyio_backend)

65 if has_backend_arg:

66 kwargs["anyio_backend"] = anyio_backend

67

68 with get_runner(backend_name, backend_options) as runner:

69 if isasyncgenfunction(func):

70 yield from runner.run_asyncgen_fixture(func, kwargs)

71 else:

72 yield runner.run_fixture(func, kwargs)

73

74 # Only apply this to coroutine functions and async generator functions in requests that involve

75 # the anyio_backend fixture

76 func = fixturedef.func

77 if isasyncgenfunction(func) or iscoroutinefunction(func):

78 if "anyio_backend" in request.fixturenames:

79 has_backend_arg = "anyio_backend" in fixturedef.argnames

80 fixturedef.func = wrapper

81 if not has_backend_arg:

82 fixturedef.argnames += ("anyio_backend",)

83

84

85@pytest.hookimpl(tryfirst=True)

86 def pytest_pycollect_makeitem(collector: Any, name: Any, obj: Any) -> None:

87 if collector.istestfunction(obj, name):

88 inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj

89 if iscoroutinefunction(inner_func):

90 marker = collector.get_closest_marker("anyio")

91 own_markers = getattr(obj, "pytestmark", ())

92 if marker or any(marker.name == "anyio" for marker in own_markers):

93 pytest.mark.usefixtures("anyio_backend")(obj)

94

95

96@pytest.hookimpl(tryfirst=True)

97 def pytest_pyfunc_call(pyfuncitem: Any) -> Optional[bool]:

98 def run_with_hypothesis(**kwargs: Any) -> None:

99 with get_runner(backend_name, backend_options) as runner:

100 runner.run_test(original_func, kwargs)

101

102 backend = pyfuncitem.funcargs.get("anyio_backend")

103 if backend:

104 backend_name, backend_options = extract_backend_and_options(backend)

105

106 if hasattr(pyfuncitem.obj, "hypothesis"):

107 # Wrap the inner test function unless it's already wrapped

108 original_func = pyfuncitem.obj.hypothesis.inner_test

109 if original_func.__qualname__ != run_with_hypothesis.__qualname__:

110 if iscoroutinefunction(original_func):

111 pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis

112

113 return None

114

115 if iscoroutinefunction(pyfuncitem.obj):

116 funcargs = pyfuncitem.funcargs

117 testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}

118 with get_runner(backend_name, backend_options) as runner:

119 runner.run_test(pyfuncitem.obj, testargs)

120

121 return True

122

123 return None

124

125

126@pytest.fixture(params=get_all_backends())

127 def anyio_backend(request: Any) -> Any:

128 return request.param

129

130

131@pytest.fixture

132 def anyio_backend_name(anyio_backend: Any) -> str:

133 if isinstance(anyio_backend, str):

134 return anyio_backend

135 else:

136 return anyio_backend[0]

137

138

139@pytest.fixture

140 def anyio_backend_options(anyio_backend: Any) -> Dict[str, Any]:

141 if isinstance(anyio_backend, str):

142 return {}

143 else:

144 return anyio_backend[1]