11"""pytest-asyncio implementation."""
22import asyncio
33import contextlib
4+ import enum
45import functools
56import inspect
67import socket
8+ import sys
9+ import warnings
710
811import pytest
912
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
1186
1287
1388def _is_coroutine (obj ):
1489 """Check to see if an object is really an asyncio coroutine."""
1590 return asyncio .iscoroutinefunction (obj ) or inspect .isgeneratorfunction (obj )
1691
1792
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+
18104def pytest_configure (config ):
19105 """Inject documentation."""
20106 config .addinivalue_line (
@@ -23,6 +109,22 @@ def pytest_configure(config):
23109 "mark the test as a coroutine, it will be "
24110 "run using an asyncio event loop" ,
25111 )
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+ )
26128
27129
28130@pytest .mark .tryfirst
@@ -32,6 +134,13 @@ def pytest_pycollect_makeitem(collector, name, obj):
32134 item = pytest .Function .from_parent (collector , name = name )
33135 if "asyncio" in item .keywords :
34136 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
35144
36145
37146class FixtureStripper :
@@ -88,9 +197,42 @@ def pytest_fixture_setup(fixturedef, request):
88197 policy .set_event_loop (loop )
89198 return
90199
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 ):
92234 # This is an async generator function. Wrap it accordingly.
93- generator = fixturedef . func
235+ generator = func
94236
95237 fixture_stripper = FixtureStripper (fixturedef )
96238 fixture_stripper .add (FixtureStripper .EVENT_LOOP )
@@ -129,8 +271,8 @@ async def async_finalizer():
129271 return loop .run_until_complete (setup ())
130272
131273 fixturedef .func = wrapper
132- elif inspect .iscoroutinefunction (fixturedef . func ):
133- coro = fixturedef . func
274+ elif inspect .iscoroutinefunction (func ):
275+ coro = func
134276
135277 fixture_stripper = FixtureStripper (fixturedef )
136278 fixture_stripper .add (FixtureStripper .EVENT_LOOP )
0 commit comments