Skip to content

Commit 907c461

Browse files
authored
Refactor pytest_pycollect_makeitems (#421)
* refactor: Extracted logic for marking tests in auto mode into pytest_collection_modifyitems. pytest_pycollect_makeitem currently calls `Collector._genfunctions`, in order to delegate further collection of test items to the current pytest collector. It does so only to add the asyncio mark to async tests after the items have been collected. Rather than relying on a call to the protected `Collector._genfunctions` method the marking logic was moved to the pytest_collection_modifyitems hook, which is called at the end of the collection phase. This change removes the call to protected functions and makes the code easier to understand. Signed-off-by: Michael Seifert <[email protected]> * refactor: Hoist up check for asyncio mode before trying to modify function items. pytest_collection_modifyitems has no effect when asyncio mode is not set to AUTO. Moving the mode check out of the loop prevents unnecessary work. Signed-off-by: Michael Seifert <[email protected]> * refactor: Renamed _set_explicit_asyncio_mark and _has_explicit_asyncio_mark to _make_asyncio_fixture_function and _is_asyncio_fixture_function, respectively. The new names reflect the purpose of the functions, instead of what they do. The new names also avoid confusion with pytest markers by not using "mark" in their names. Signed-off-by: Michael Seifert <[email protected]> * refactor: Removed obsolete elif clause. Legacy mode has been removed, so we don't need an elif to check if we're in AUTO mode. Signed-off-by: Michael Seifert <[email protected]> * refactor: Renamed the "holder" argument to _preprocess_async_fixtures to "processed_fixturedefs" to better reflect the purpose of the variable. Signed-off-by: Michael Seifert <[email protected]> * refactor: Simplified branching structure of _preprocess_async_fixtures. It is safe to call _make_asyncio_fixture_function without checking whether the fixture function has been converted to an asyncio fixture function, because each fixture is only processed once in the loop. Signed-off-by: Michael Seifert <[email protected]> * refactor: Simplified logic in _preprocess_async_fixtures. Merged two if-clauses both of which cause the current fixturedef to be skipped. Signed-off-by: Michael Seifert <[email protected]> * refactor: Extracted _inject_fixture_argnames from _preprocess_async_fixtures in order to improve readability. Signed-off-by: Michael Seifert <[email protected]> * refactor: Extracted _synchronize_async_fixture from _preprocess_async_fixtures in order to improve readability. Signed-off-by: Michael Seifert <[email protected]>
1 parent d45ab21 commit 907c461

File tree

1 file changed

+69
-48
lines changed

1 file changed

+69
-48
lines changed

pytest_asyncio/plugin.py

Lines changed: 69 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626

2727
import pytest
28+
from pytest import Function, Session, Item
2829

2930
if sys.version_info >= (3, 8):
3031
from typing import Literal
@@ -121,7 +122,7 @@ def fixture(
121122
fixture_function: Optional[FixtureFunction] = None, **kwargs: Any
122123
) -> Union[FixtureFunction, FixtureFunctionMarker]:
123124
if fixture_function is not None:
124-
_set_explicit_asyncio_mark(fixture_function)
125+
_make_asyncio_fixture_function(fixture_function)
125126
return pytest.fixture(fixture_function, **kwargs)
126127

127128
else:
@@ -133,12 +134,12 @@ def inner(fixture_function: FixtureFunction) -> FixtureFunction:
133134
return inner
134135

135136

136-
def _has_explicit_asyncio_mark(obj: Any) -> bool:
137+
def _is_asyncio_fixture_function(obj: Any) -> bool:
137138
obj = getattr(obj, "__func__", obj) # instance method maybe?
138139
return getattr(obj, "_force_asyncio_fixture", False)
139140

140141

141-
def _set_explicit_asyncio_mark(obj: Any) -> None:
142+
def _make_asyncio_fixture_function(obj: Any) -> None:
142143
if hasattr(obj, "__func__"):
143144
# instance method, check the function object
144145
obj = obj.__func__
@@ -186,41 +187,51 @@ def pytest_report_header(config: Config) -> List[str]:
186187
return [f"asyncio: mode={mode}"]
187188

188189

189-
def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None:
190+
def _preprocess_async_fixtures(
191+
config: Config,
192+
processed_fixturedefs: Set[FixtureDef],
193+
) -> None:
190194
asyncio_mode = _get_asyncio_mode(config)
191195
fixturemanager = config.pluginmanager.get_plugin("funcmanage")
192196
for fixtures in fixturemanager._arg2fixturedefs.values():
193197
for fixturedef in fixtures:
194-
if fixturedef in holder:
195-
continue
196198
func = fixturedef.func
197-
if not _is_coroutine_or_asyncgen(func):
198-
# Nothing to do with a regular fixture function
199+
if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen(
200+
func
201+
):
202+
continue
203+
if not _is_asyncio_fixture_function(func) and asyncio_mode == Mode.STRICT:
204+
# Ignore async fixtures without explicit asyncio mark in strict mode
205+
# This applies to pytest_trio fixtures, for example
199206
continue
200-
if not _has_explicit_asyncio_mark(func):
201-
if asyncio_mode == Mode.STRICT:
202-
# Ignore async fixtures without explicit asyncio mark in strict mode
203-
# This applies to pytest_trio fixtures, for example
204-
continue
205-
elif asyncio_mode == Mode.AUTO:
206-
# Enforce asyncio mode if 'auto'
207-
_set_explicit_asyncio_mark(func)
207+
_make_asyncio_fixture_function(func)
208+
_inject_fixture_argnames(fixturedef)
209+
_synchronize_async_fixture(fixturedef)
210+
assert _is_asyncio_fixture_function(fixturedef.func)
211+
processed_fixturedefs.add(fixturedef)
208212

209-
to_add = []
210-
for name in ("request", "event_loop"):
211-
if name not in fixturedef.argnames:
212-
to_add.append(name)
213213

214-
if to_add:
215-
fixturedef.argnames += tuple(to_add)
214+
def _inject_fixture_argnames(fixturedef: FixtureDef) -> None:
215+
"""
216+
Ensures that `request` and `event_loop` are arguments of the specified fixture.
217+
"""
218+
to_add = []
219+
for name in ("request", "event_loop"):
220+
if name not in fixturedef.argnames:
221+
to_add.append(name)
222+
if to_add:
223+
fixturedef.argnames += tuple(to_add)
216224

217-
if inspect.isasyncgenfunction(func):
218-
fixturedef.func = _wrap_asyncgen(func)
219-
elif inspect.iscoroutinefunction(func):
220-
fixturedef.func = _wrap_async(func)
221225

222-
assert _has_explicit_asyncio_mark(fixturedef.func)
223-
holder.add(fixturedef)
226+
def _synchronize_async_fixture(fixturedef: FixtureDef) -> None:
227+
"""
228+
Wraps the fixture function of an async fixture in a synchronous function.
229+
"""
230+
func = fixturedef.func
231+
if inspect.isasyncgenfunction(func):
232+
fixturedef.func = _wrap_asyncgen(func)
233+
elif inspect.iscoroutinefunction(func):
234+
fixturedef.func = _wrap_async(func)
224235

225236

226237
def _add_kwargs(
@@ -290,36 +301,46 @@ async def setup() -> _R:
290301

291302
@pytest.mark.tryfirst
292303
def pytest_pycollect_makeitem(
293-
collector: Union[pytest.Module, pytest.Class], name: str, obj: object
304+
collector: Union[pytest.Module, pytest.Class], name: str, obj: object
294305
) -> Union[
295306
None, pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]]
296307
]:
297308
"""A pytest hook to collect asyncio coroutines."""
298309
if not collector.funcnamefilter(name):
299310
return None
300311
_preprocess_async_fixtures(collector.config, _HOLDER)
301-
if isinstance(obj, staticmethod):
302-
# staticmethods need to be unwrapped.
303-
obj = obj.__func__
304-
if (
305-
_is_coroutine(obj)
306-
or _is_hypothesis_test(obj)
307-
and _hypothesis_test_wraps_coroutine(obj)
308-
):
309-
item = pytest.Function.from_parent(collector, name=name)
310-
marker = item.get_closest_marker("asyncio")
311-
if marker is not None:
312-
return list(collector._genfunctions(name, obj))
313-
else:
314-
if _get_asyncio_mode(item.config) == Mode.AUTO:
315-
# implicitly add asyncio marker if asyncio mode is on
316-
ret = list(collector._genfunctions(name, obj))
317-
for elem in ret:
318-
elem.add_marker("asyncio")
319-
return ret # type: ignore[return-value]
320312
return None
321313

322314

315+
def pytest_collection_modifyitems(
316+
session: Session, config: Config, items: List[Item]
317+
) -> None:
318+
"""
319+
Marks collected async test items as `asyncio` tests.
320+
321+
The mark is only applied in `AUTO` mode. It is applied to:
322+
323+
- coroutines
324+
- staticmethods wrapping coroutines
325+
- Hypothesis tests wrapping coroutines
326+
327+
"""
328+
if _get_asyncio_mode(config) != Mode.AUTO:
329+
return
330+
function_items = (item for item in items if isinstance(item, Function))
331+
for function_item in function_items:
332+
function = function_item.obj
333+
if isinstance(function, staticmethod):
334+
# staticmethods need to be unwrapped.
335+
function = function.__func__
336+
if (
337+
_is_coroutine(function)
338+
or _is_hypothesis_test(function)
339+
and _hypothesis_test_wraps_coroutine(function)
340+
):
341+
function_item.add_marker("asyncio")
342+
343+
323344
def _hypothesis_test_wraps_coroutine(function: Any) -> bool:
324345
return _is_coroutine(function.hypothesis.inner_test)
325346

0 commit comments

Comments
 (0)