Skip to content

Commit 7dbad55

Browse files
tjkusonseifertm
authored andcommitted
Skip unavailable requested loop factories
1 parent 91edbbe commit 7dbad55

File tree

4 files changed

+100
-27
lines changed

4 files changed

+100
-27
lines changed

docs/how-to-guides/run_test_with_specific_loop_factories.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ To run a test with only a subset of configured factories, use the ``loop_factori
1212
@pytest.mark.asyncio(loop_factories=["custom"])
1313
async def test_only_with_custom_event_loop():
1414
pass
15+
16+
If a requested factory name is not available from the hook, the test variant for that factory is skipped.

docs/reference/markers/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Subpackages do not share the loop with their parent package.
4343

4444
Tests marked with *session* scope share the same event loop, even if the tests exist in different packages.
4545

46-
The ``pytest.mark.asyncio`` marker also accepts a ``loop_factories`` keyword argument to select a subset of configured event loop factories for a test. If ``loop_factories`` contains unknown names, pytest-asyncio raises a ``pytest.UsageError`` during collection.
46+
The ``pytest.mark.asyncio`` marker also accepts a ``loop_factories`` keyword argument to select a subset of configured event loop factories for a test. If ``loop_factories`` contains names not available from the hook, those test variants are skipped.
4747

4848
.. |auto mode| replace:: *auto mode*
4949
.. _auto mode: ../../concepts.html#auto-mode

pytest_asyncio/plugin.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AsyncIterator,
1818
Awaitable,
1919
Callable,
20+
Collection,
2021
Generator,
2122
Iterable,
2223
Iterator,
@@ -746,32 +747,41 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
746747
)
747748
return
748749

750+
factory_params: Collection[object]
751+
factory_ids: Collection[str]
749752
if marker_selected_factory_names is None:
750-
effective_factories = hook_factories
753+
factory_params = hook_factories.values()
754+
factory_ids = hook_factories.keys()
751755
else:
752-
missing_factory_names = tuple(
753-
name for name in marker_selected_factory_names if name not in hook_factories
754-
)
755-
if missing_factory_names:
756-
msg = (
757-
f"Unknown factory name(s) {missing_factory_names}."
758-
f" Available names: {', '.join(hook_factories)}."
756+
# Iterate in marker order to preserve explicit user selection
757+
# order.
758+
factory_ids = marker_selected_factory_names
759+
factory_params = [
760+
(
761+
hook_factories[name]
762+
if name in hook_factories
763+
else pytest.param(
764+
None,
765+
marks=pytest.mark.skip(
766+
reason=(
767+
f"Loop factory {name!r} is not available."
768+
f" Available factories:"
769+
f" {', '.join(hook_factories)}."
770+
),
771+
),
772+
)
759773
)
760-
raise pytest.UsageError(msg)
761-
# Build the mapping in marker order to preserve explicit user
762-
# selection order in parametrization.
763-
effective_factories = {
764-
name: hook_factories[name] for name in marker_selected_factory_names
765-
}
774+
for name in marker_selected_factory_names
775+
]
766776
metafunc.fixturenames.append(_asyncio_loop_factory.__name__)
767777
default_loop_scope = _get_default_test_loop_scope(metafunc.config)
768778
loop_scope = marker_loop_scope or default_loop_scope
769779
# pytest.HIDDEN_PARAM was added in pytest 8.4
770-
hide_id = len(effective_factories) == 1 and hasattr(pytest, "HIDDEN_PARAM")
780+
hide_id = len(factory_ids) == 1 and hasattr(pytest, "HIDDEN_PARAM")
771781
metafunc.parametrize(
772782
_asyncio_loop_factory.__name__,
773-
effective_factories.values(),
774-
ids=(pytest.HIDDEN_PARAM,) if hide_id else effective_factories.keys(),
783+
factory_params,
784+
ids=(pytest.HIDDEN_PARAM,) if hide_id else factory_ids,
775785
indirect=True,
776786
scope=loop_scope,
777787
)

tests/test_loop_factory_parametrization.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ async def test_runs_only_with_uvloop():
400400
result.assert_outcomes(passed=1)
401401

402402

403-
def test_asyncio_marker_loop_factories_unknown_name_errors(pytester: Pytester) -> None:
403+
def test_unavailable_factory_skips_with_reason(pytester: Pytester) -> None:
404404
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
405405
pytester.makeconftest(dedent("""\
406406
import asyncio
@@ -414,16 +414,77 @@ def pytest_asyncio_loop_factories(config, item):
414414
pytest_plugins = "pytest_asyncio"
415415
416416
@pytest.mark.asyncio(loop_factories=["missing"])
417-
async def test_errors():
417+
async def test_skipped():
418418
assert True
419419
"""))
420-
result = pytester.runpytest("--asyncio-mode=strict")
421-
result.assert_outcomes(errors=1)
422-
result.stdout.fnmatch_lines(
423-
[
424-
"*Unknown factory name(s)*Available names:*",
425-
]
426-
)
420+
result = pytester.runpytest("--asyncio-mode=strict", "-rs")
421+
result.assert_outcomes(skipped=1)
422+
result.stdout.fnmatch_lines(["*SKIPPED*Loop factory 'missing' is not available*"])
423+
424+
425+
def test_partial_intersection_runs_available_and_skips_missing(
426+
pytester: Pytester,
427+
) -> None:
428+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
429+
pytester.makeconftest(dedent("""\
430+
import asyncio
431+
432+
class CustomEventLoop(asyncio.SelectorEventLoop):
433+
pass
434+
435+
def pytest_asyncio_loop_factories(config, item):
436+
return {
437+
"available": CustomEventLoop,
438+
"other": asyncio.new_event_loop,
439+
}
440+
"""))
441+
pytester.makepyfile(dedent("""\
442+
import asyncio
443+
import pytest
444+
445+
pytest_plugins = "pytest_asyncio"
446+
447+
@pytest.mark.asyncio(loop_factories=["available", "missing"])
448+
async def test_runs_with_available():
449+
assert type(asyncio.get_running_loop()).__name__ == "CustomEventLoop"
450+
"""))
451+
result = pytester.runpytest("--asyncio-mode=strict", "-rs")
452+
result.assert_outcomes(passed=1, skipped=1)
453+
result.stdout.fnmatch_lines(["*SKIPPED*Loop factory 'missing' is not available*"])
454+
455+
456+
def test_platform_conditional_factories(pytester: Pytester) -> None:
457+
pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function")
458+
pytester.makeconftest(dedent("""\
459+
import asyncio
460+
import sys
461+
462+
def pytest_asyncio_loop_factories(config, item):
463+
factories = {"default": asyncio.new_event_loop}
464+
if sys.platform == "a_platform_that_does_not_exist":
465+
factories["exotic"] = asyncio.new_event_loop
466+
return factories
467+
"""))
468+
pytester.makepyfile(dedent("""\
469+
import pytest
470+
471+
pytest_plugins = "pytest_asyncio"
472+
473+
@pytest.mark.asyncio(loop_factories=["exotic"])
474+
async def test_exotic_only():
475+
assert True
476+
477+
@pytest.mark.asyncio(loop_factories=["default"])
478+
async def test_default_only():
479+
assert True
480+
481+
@pytest.mark.asyncio(loop_factories=["default", "exotic"])
482+
async def test_both():
483+
assert True
484+
"""))
485+
result = pytester.runpytest("--asyncio-mode=strict", "-rs")
486+
result.assert_outcomes(passed=2, skipped=2)
487+
result.stdout.fnmatch_lines(["*SKIPPED*Loop factory 'exotic' is not available*"])
427488

428489

429490
def test_asyncio_marker_loop_factories_without_hook_errors(

0 commit comments

Comments
 (0)