From 3f18811922ece09dd51a4839f47faa72f0ca4b08 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 2 Oct 2022 12:38:12 +0200 Subject: [PATCH 1/9] 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 --- pytest_asyncio/plugin.py | 50 ++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 6fd300d5..14edd68e 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -24,6 +24,7 @@ ) import pytest +from pytest import Function, Session, Item if sys.version_info >= (3, 8): from typing import Literal @@ -281,7 +282,7 @@ async def setup() -> _R: @pytest.mark.tryfirst def pytest_pycollect_makeitem( - collector: Union[pytest.Module, pytest.Class], name: str, obj: object + collector: Union[pytest.Module, pytest.Class], name: str, obj: object ) -> Union[ None, pytest.Item, pytest.Collector, List[Union[pytest.Item, pytest.Collector]] ]: @@ -289,28 +290,37 @@ def pytest_pycollect_makeitem( if not collector.funcnamefilter(name): return None _preprocess_async_fixtures(collector.config, _HOLDER) - if isinstance(obj, staticmethod): - # staticmethods need to be unwrapped. - obj = obj.__func__ - if ( - _is_coroutine(obj) - or _is_hypothesis_test(obj) - and _hypothesis_test_wraps_coroutine(obj) - ): - item = pytest.Function.from_parent(collector, name=name) - marker = item.get_closest_marker("asyncio") - if marker is not None: - return list(collector._genfunctions(name, obj)) - else: - if _get_asyncio_mode(item.config) == Mode.AUTO: - # implicitly add asyncio marker if asyncio mode is on - ret = list(collector._genfunctions(name, obj)) - for elem in ret: - elem.add_marker("asyncio") - return ret # type: ignore[return-value] return None +def pytest_collection_modifyitems( + session: Session, config: Config, items: List[Item] +) -> None: + """ + Marks collected async test items as `asyncio` tests. + + The mark is only applied in `AUTO` mode. It is applied to: + + - coroutines + - staticmethods wrapping coroutines + - Hypothesis tests wrapping coroutines + + """ + function_items = (item for item in items if isinstance(item, Function)) + for function_item in function_items: + function = function_item.obj + if isinstance(function, staticmethod): + # staticmethods need to be unwrapped. + function = function.__func__ + if ( + _is_coroutine(function) + or _is_hypothesis_test(function) + and _hypothesis_test_wraps_coroutine(function) + ): + if _get_asyncio_mode(config) == Mode.AUTO: + function_item.add_marker("asyncio") + + def _hypothesis_test_wraps_coroutine(function: Any) -> bool: return _is_coroutine(function.hypothesis.inner_test) From f25c5cc69d0a9ffb6561ae6b2ef3fc4e4cd15601 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sun, 2 Oct 2022 12:42:46 +0200 Subject: [PATCH 2/9] 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 --- pytest_asyncio/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 14edd68e..f3b4318f 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -306,6 +306,8 @@ def pytest_collection_modifyitems( - Hypothesis tests wrapping coroutines """ + if _get_asyncio_mode(config) != Mode.AUTO: + return function_items = (item for item in items if isinstance(item, Function)) for function_item in function_items: function = function_item.obj @@ -317,8 +319,7 @@ def pytest_collection_modifyitems( or _is_hypothesis_test(function) and _hypothesis_test_wraps_coroutine(function) ): - if _get_asyncio_mode(config) == Mode.AUTO: - function_item.add_marker("asyncio") + function_item.add_marker("asyncio") def _hypothesis_test_wraps_coroutine(function: Any) -> bool: From 7485d706fa108ae37f11f5c26a58e8bcc1405982 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 09:18:10 +0200 Subject: [PATCH 3/9] 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 --- pytest_asyncio/plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index f3b4318f..de09b34b 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -121,7 +121,7 @@ def fixture( fixture_function: Optional[FixtureFunction] = None, **kwargs: Any ) -> Union[FixtureFunction, FixtureFunctionMarker]: if fixture_function is not None: - _set_explicit_asyncio_mark(fixture_function) + _make_asyncio_fixture_function(fixture_function) return pytest.fixture(fixture_function, **kwargs) else: @@ -133,12 +133,12 @@ def inner(fixture_function: FixtureFunction) -> FixtureFunction: return inner -def _has_explicit_asyncio_mark(obj: Any) -> bool: +def _is_asyncio_fixture_function(obj: Any) -> bool: obj = getattr(obj, "__func__", obj) # instance method maybe? return getattr(obj, "_force_asyncio_fixture", False) -def _set_explicit_asyncio_mark(obj: Any) -> None: +def _make_asyncio_fixture_function(obj: Any) -> None: if hasattr(obj, "__func__"): # instance method, check the function object obj = obj.__func__ @@ -189,14 +189,14 @@ def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None: if not _is_coroutine_or_asyncgen(func): # Nothing to do with a regular fixture function continue - if not _has_explicit_asyncio_mark(func): + if not _is_asyncio_fixture_function(func): if asyncio_mode == Mode.STRICT: # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue elif asyncio_mode == Mode.AUTO: # Enforce asyncio mode if 'auto' - _set_explicit_asyncio_mark(func) + _make_asyncio_fixture_function(func) to_add = [] for name in ("request", "event_loop"): @@ -211,7 +211,7 @@ def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None: elif inspect.iscoroutinefunction(func): fixturedef.func = _wrap_async(func) - assert _has_explicit_asyncio_mark(fixturedef.func) + assert _is_asyncio_fixture_function(fixturedef.func) holder.add(fixturedef) From a0b9fd620409c13feffa769f21db0e478e5ad386 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 09:35:42 +0200 Subject: [PATCH 4/9] 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 --- pytest_asyncio/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index de09b34b..2395fc31 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -194,9 +194,8 @@ def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None: # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue - elif asyncio_mode == Mode.AUTO: - # Enforce asyncio mode if 'auto' - _make_asyncio_fixture_function(func) + # Enforce asyncio mode if 'auto' + _make_asyncio_fixture_function(func) to_add = [] for name in ("request", "event_loop"): From 97d5e32669534153c2058f6c6d217dbd5c7dd6ce Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 09:37:15 +0200 Subject: [PATCH 5/9] 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 --- pytest_asyncio/plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 2395fc31..6b2bfe31 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -178,12 +178,15 @@ def pytest_report_header(config: Config) -> List[str]: return [f"asyncio: mode={mode}"] -def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None: +def _preprocess_async_fixtures( + config: Config, + processed_fixturedefs: Set[FixtureDef], +) -> None: asyncio_mode = _get_asyncio_mode(config) fixturemanager = config.pluginmanager.get_plugin("funcmanage") for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: - if fixturedef in holder: + if fixturedef in processed_fixturedefs: continue func = fixturedef.func if not _is_coroutine_or_asyncgen(func): @@ -211,7 +214,7 @@ def _preprocess_async_fixtures(config: Config, holder: Set[FixtureDef]) -> None: fixturedef.func = _wrap_async(func) assert _is_asyncio_fixture_function(fixturedef.func) - holder.add(fixturedef) + processed_fixturedefs.add(fixturedef) def _add_kwargs( From d9cfe5cf0e047934db15f4783d767edd248a5b7a Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 10:03:27 +0200 Subject: [PATCH 6/9] 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 --- pytest_asyncio/plugin.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 6b2bfe31..d37dbff8 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -192,13 +192,11 @@ def _preprocess_async_fixtures( if not _is_coroutine_or_asyncgen(func): # Nothing to do with a regular fixture function continue - if not _is_asyncio_fixture_function(func): - if asyncio_mode == Mode.STRICT: - # Ignore async fixtures without explicit asyncio mark in strict mode - # This applies to pytest_trio fixtures, for example - continue - # Enforce asyncio mode if 'auto' - _make_asyncio_fixture_function(func) + if not _is_asyncio_fixture_function(func) and asyncio_mode == Mode.STRICT: + # Ignore async fixtures without explicit asyncio mark in strict mode + # This applies to pytest_trio fixtures, for example + continue + _make_asyncio_fixture_function(func) to_add = [] for name in ("request", "event_loop"): From e49a09337a45ad68aa18f6523c44c619b2152713 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 10:44:20 +0200 Subject: [PATCH 7/9] 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 --- pytest_asyncio/plugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index d37dbff8..62acf208 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -186,11 +186,10 @@ def _preprocess_async_fixtures( fixturemanager = config.pluginmanager.get_plugin("funcmanage") for fixtures in fixturemanager._arg2fixturedefs.values(): for fixturedef in fixtures: - if fixturedef in processed_fixturedefs: - continue func = fixturedef.func - if not _is_coroutine_or_asyncgen(func): - # Nothing to do with a regular fixture function + if fixturedef in processed_fixturedefs or not _is_coroutine_or_asyncgen( + func + ): continue if not _is_asyncio_fixture_function(func) and asyncio_mode == Mode.STRICT: # Ignore async fixtures without explicit asyncio mark in strict mode From f539b7ebcf7a89d0c7205347c6ab8fd233304240 Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 10:51:28 +0200 Subject: [PATCH 8/9] refactor: Extracted _inject_fixture_argnames from _preprocess_async_fixtures in order to improve readability. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 62acf208..d25133cd 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -196,15 +196,7 @@ def _preprocess_async_fixtures( # This applies to pytest_trio fixtures, for example continue _make_asyncio_fixture_function(func) - - to_add = [] - for name in ("request", "event_loop"): - if name not in fixturedef.argnames: - to_add.append(name) - - if to_add: - fixturedef.argnames += tuple(to_add) - + _inject_fixture_argnames(fixturedef) if inspect.isasyncgenfunction(func): fixturedef.func = _wrap_asyncgen(func) elif inspect.iscoroutinefunction(func): @@ -214,6 +206,18 @@ def _preprocess_async_fixtures( processed_fixturedefs.add(fixturedef) +def _inject_fixture_argnames(fixturedef: FixtureDef) -> None: + """ + Ensures that `request` and `event_loop` are arguments of the specified fixture. + """ + to_add = [] + for name in ("request", "event_loop"): + if name not in fixturedef.argnames: + to_add.append(name) + if to_add: + fixturedef.argnames += tuple(to_add) + + def _add_kwargs( func: Callable[..., Any], kwargs: Dict[str, Any], From 0a30818c120cfe15744f6375c8e6cc8cc854622d Mon Sep 17 00:00:00 2001 From: Michael Seifert Date: Sat, 8 Oct 2022 10:59:16 +0200 Subject: [PATCH 9/9] refactor: Extracted _synchronize_async_fixture from _preprocess_async_fixtures in order to improve readability. Signed-off-by: Michael Seifert --- pytest_asyncio/plugin.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index d25133cd..b3dc9d67 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -197,11 +197,7 @@ def _preprocess_async_fixtures( continue _make_asyncio_fixture_function(func) _inject_fixture_argnames(fixturedef) - if inspect.isasyncgenfunction(func): - fixturedef.func = _wrap_asyncgen(func) - elif inspect.iscoroutinefunction(func): - fixturedef.func = _wrap_async(func) - + _synchronize_async_fixture(fixturedef) assert _is_asyncio_fixture_function(fixturedef.func) processed_fixturedefs.add(fixturedef) @@ -218,6 +214,17 @@ def _inject_fixture_argnames(fixturedef: FixtureDef) -> None: fixturedef.argnames += tuple(to_add) +def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: + """ + Wraps the fixture function of an async fixture in a synchronous function. + """ + func = fixturedef.func + if inspect.isasyncgenfunction(func): + fixturedef.func = _wrap_asyncgen(func) + elif inspect.iscoroutinefunction(func): + fixturedef.func = _wrap_async(func) + + def _add_kwargs( func: Callable[..., Any], kwargs: Dict[str, Any],