1
1
"""pytest-asyncio implementation."""
2
2
import asyncio
3
3
import contextlib
4
+ import enum
4
5
import functools
5
6
import inspect
6
7
import socket
8
+ import sys
9
+ import warnings
7
10
8
11
import pytest
9
12
10
- from inspect import isasyncgenfunction
13
+
14
+ class Mode (str , enum .Enum ):
15
+ AUTO = "auto"
16
+ STRICT = "strict"
17
+ LEGACY = "legacy"
18
+
19
+
20
+ LEGACY_MODE = pytest .PytestDeprecationWarning (
21
+ "The 'asyncio_mode' default value will change to 'strict' in future, "
22
+ "please explicitly use 'asyncio_mode=strict' or 'asyncio_mode=auto' "
23
+ "in pytest configuration file."
24
+ )
25
+
26
+ LEGACY_ASYNCIO_FIXTURE = (
27
+ "'@pytest.fixture' is applied to {name} "
28
+ "in 'legacy' mode, "
29
+ "please replace it with '@pytest_asyncio.fixture' as a preparation "
30
+ "for switching to 'strict' mode (or use 'auto' mode to seamlessly handle "
31
+ "all these fixtures as asyncio-driven)."
32
+ )
33
+
34
+
35
+ ASYNCIO_MODE_HELP = """\
36
+ 'auto' - for automatically handling all async functions by the plugin
37
+ 'strict' - for autoprocessing disabling (useful if different async frameworks \
38
+ should be tested together, e.g. \
39
+ both pytest-asyncio and pytest-trio are used in the same project)
40
+ 'legacy' - for keeping compatibility with pytest-asyncio<0.17: \
41
+ auto-handling is disabled but pytest_asyncio.fixture usage is not enforced
42
+ """
43
+
44
+
45
+ def pytest_addoption (parser , pluginmanager ):
46
+ group = parser .getgroup ("asyncio" )
47
+ group .addoption (
48
+ "--asyncio-mode" ,
49
+ dest = "asyncio_mode" ,
50
+ default = None ,
51
+ metavar = "MODE" ,
52
+ help = ASYNCIO_MODE_HELP ,
53
+ )
54
+ parser .addini (
55
+ "asyncio_mode" ,
56
+ help = "default value for --asyncio-mode" ,
57
+ type = "string" ,
58
+ default = "legacy" ,
59
+ )
60
+
61
+
62
+ def fixture (fixture_function = None , ** kwargs ):
63
+ if fixture_function is not None :
64
+ _set_explicit_asyncio_mark (fixture_function )
65
+ return pytest .fixture (fixture_function , ** kwargs )
66
+
67
+ else :
68
+
69
+ @functools .wraps (fixture )
70
+ def inner (fixture_function ):
71
+ return fixture (fixture_function , ** kwargs )
72
+
73
+ return inner
74
+
75
+
76
+ def _has_explicit_asyncio_mark (obj ):
77
+ obj = getattr (obj , "__func__" , obj ) # instance method maybe?
78
+ return getattr (obj , "_force_asyncio_fixture" , False )
79
+
80
+
81
+ def _set_explicit_asyncio_mark (obj ):
82
+ if hasattr (obj , "__func__" ):
83
+ # instance method, check the function object
84
+ obj = obj .__func__
85
+ obj ._force_asyncio_fixture = True
11
86
12
87
13
88
def _is_coroutine (obj ):
14
89
"""Check to see if an object is really an asyncio coroutine."""
15
90
return asyncio .iscoroutinefunction (obj ) or inspect .isgeneratorfunction (obj )
16
91
17
92
93
+ def _is_coroutine_or_asyncgen (obj ):
94
+ return _is_coroutine (obj ) or inspect .isasyncgenfunction (obj )
95
+
96
+
97
+ def _get_asyncio_mode (config ):
98
+ val = config .getoption ("asyncio_mode" )
99
+ if val is None :
100
+ val = config .getini ("asyncio_mode" )
101
+ return Mode (val )
102
+
103
+
18
104
def pytest_configure (config ):
19
105
"""Inject documentation."""
20
106
config .addinivalue_line (
@@ -23,6 +109,22 @@ def pytest_configure(config):
23
109
"mark the test as a coroutine, it will be "
24
110
"run using an asyncio event loop" ,
25
111
)
112
+ if _get_asyncio_mode (config ) == Mode .LEGACY :
113
+ _issue_warning_captured (LEGACY_MODE , config .hook , stacklevel = 1 )
114
+
115
+
116
+ def _issue_warning_captured (warning , hook , * , stacklevel = 1 ):
117
+ # copy-paste of pytest internal _pytest.warnings._issue_warning_captured function
118
+ with warnings .catch_warnings (record = True ) as records :
119
+ warnings .simplefilter ("always" , type (warning ))
120
+ warnings .warn (LEGACY_MODE , stacklevel = stacklevel )
121
+ frame = sys ._getframe (stacklevel - 1 )
122
+ location = frame .f_code .co_filename , frame .f_lineno , frame .f_code .co_name
123
+ hook .pytest_warning_recorded .call_historic (
124
+ kwargs = dict (
125
+ warning_message = records [0 ], when = "config" , nodeid = "" , location = location
126
+ )
127
+ )
26
128
27
129
28
130
@pytest .mark .tryfirst
@@ -32,6 +134,13 @@ def pytest_pycollect_makeitem(collector, name, obj):
32
134
item = pytest .Function .from_parent (collector , name = name )
33
135
if "asyncio" in item .keywords :
34
136
return list (collector ._genfunctions (name , obj ))
137
+ else :
138
+ if _get_asyncio_mode (item .config ) == Mode .AUTO :
139
+ # implicitly add asyncio marker if asyncio mode is on
140
+ ret = list (collector ._genfunctions (name , obj ))
141
+ for elem in ret :
142
+ elem .add_marker ("asyncio" )
143
+ return ret
35
144
36
145
37
146
class FixtureStripper :
@@ -88,9 +197,42 @@ def pytest_fixture_setup(fixturedef, request):
88
197
policy .set_event_loop (loop )
89
198
return
90
199
91
- if isasyncgenfunction (fixturedef .func ):
200
+ func = fixturedef .func
201
+ if not _is_coroutine_or_asyncgen (func ):
202
+ # Nothing to do with a regular fixture function
203
+ yield
204
+ return
205
+
206
+ config = request .node .config
207
+ asyncio_mode = _get_asyncio_mode (config )
208
+
209
+ if not _has_explicit_asyncio_mark (func ):
210
+ if asyncio_mode == Mode .AUTO :
211
+ # Enforce asyncio mode if 'auto'
212
+ _set_explicit_asyncio_mark (func )
213
+ elif asyncio_mode == Mode .LEGACY :
214
+ _set_explicit_asyncio_mark (func )
215
+ try :
216
+ code = func .__code__
217
+ except AttributeError :
218
+ code = func .__func__ .__code__
219
+ name = (
220
+ f"<fixture { func .__qualname__ } , file={ code .co_filename } , "
221
+ f"line={ code .co_firstlineno } >"
222
+ )
223
+ warnings .warn (
224
+ LEGACY_ASYNCIO_FIXTURE .format (name = name ),
225
+ pytest .PytestDeprecationWarning ,
226
+ )
227
+ else :
228
+ # asyncio_mode is STRICT,
229
+ # don't handle fixtures that are not explicitly marked
230
+ yield
231
+ return
232
+
233
+ if inspect .isasyncgenfunction (func ):
92
234
# This is an async generator function. Wrap it accordingly.
93
- generator = fixturedef . func
235
+ generator = func
94
236
95
237
fixture_stripper = FixtureStripper (fixturedef )
96
238
fixture_stripper .add (FixtureStripper .EVENT_LOOP )
@@ -129,8 +271,8 @@ async def async_finalizer():
129
271
return loop .run_until_complete (setup ())
130
272
131
273
fixturedef .func = wrapper
132
- elif inspect .iscoroutinefunction (fixturedef . func ):
133
- coro = fixturedef . func
274
+ elif inspect .iscoroutinefunction (func ):
275
+ coro = func
134
276
135
277
fixture_stripper = FixtureStripper (fixturedef )
136
278
fixture_stripper .add (FixtureStripper .EVENT_LOOP )
0 commit comments