Skip to content

Implement event loop factory hook#1373

Merged
seifertm merged 1 commit intopytest-dev:mainfrom
tjkuson:loop-factory
Mar 25, 2026
Merged

Implement event loop factory hook#1373
seifertm merged 1 commit intopytest-dev:mainfrom
tjkuson:loop-factory

Conversation

@tjkuson
Copy link
Copy Markdown
Contributor

@tjkuson tjkuson commented Mar 7, 2026

Added the pytest_asyncio_loop_factories hook to parametrize asyncio tests with custom event loop factories. Users can use the loop_factories argument to select a subset of hooks. If omitted, the test will run parametrized with each loop factory item returned by the hook.

import asyncio
from collections.abc import Mapping

import pytest
import uvloop


def pytest_asyncio_loop_factories(
    config: pytest.Config,
    item: pytest.Item,
) -> Mapping[str, Callable[[], AbstractEventLoop]]:
    return {
        "uvloop": uvloop.new_event_loop,
        "stdlib": asyncio.new_event_loop,
    }
import pytest


@pytest.mark.asyncio(loop_factories=["uvloop"])
async def test_with_uvloop_only() -> None:
    assert True

Closes #1101, parts of #1032, #1346.

Relates to #1164 by building on the idea of having a global parametrization for all tests and fixtures instead of a marker. An alternative idea was to expose a configuration option where a user could describe event loop factors and which tests they applied to, but this seemed less ergonomic to me compared to the hook approach (and less powerful than allowing user-defined logic).

Test plan

Added new tests that pass via uvx tox.

Existing tests pass with minimumal changes.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.13%. Comparing base (ff72406) to head (6873207).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1373      +/-   ##
==========================================
+ Coverage   93.64%   95.13%   +1.49%     
==========================================
  Files           2        2              
  Lines         409      473      +64     
  Branches       44       57      +13     
==========================================
+ Hits          383      450      +67     
+ Misses         20       17       -3     
  Partials        6        6              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tjkuson tjkuson force-pushed the loop-factory branch 4 times, most recently from a8594aa to d3c835d Compare March 7, 2026 17:04
Copy link
Copy Markdown
Contributor

@seifertm seifertm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the great work @tjkuson ! This is much more than a draft. It looks like we finally have a replacement for the policy fixture in pytest-asyncio.

I did a couple of comments, but most of them are very minor. The two largest being the limitation to a single hook implementation and your thought about my idea to use the asyncio marker to limit the loop factory parametrization for a single test.

@tjkuson tjkuson force-pushed the loop-factory branch 6 times, most recently from 30a550a to 85ce9f6 Compare March 17, 2026 22:23
@tjkuson tjkuson marked this pull request as ready for review March 17, 2026 23:03
@tjkuson tjkuson requested review from Tinche and asvetlov as code owners March 17, 2026 23:03
@seifertm seifertm added this pull request to the merge queue Mar 25, 2026
@seifertm seifertm removed this pull request from the merge queue due to a manual request Mar 25, 2026
@seifertm seifertm added this pull request to the merge queue Mar 25, 2026
Merged via the queue into pytest-dev:main with commit d6e5a25 Mar 25, 2026
22 checks passed
@tjkuson tjkuson deleted the loop-factory branch March 25, 2026 17:21
Copy link
Copy Markdown
Contributor

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the work here! I've been looking how we can use that to replace our custom asyncio policy for tests in Home Assistant. So far we use something like this

asyncio.set_event_loop_policy(runner.HassEventLoopPolicyOld(debug=False))

From what I understand this could be roughly converted to

policy = runner.HassEventLoopPolicy(True)

def pytest_asyncio_loop_factories(
    config: pytest.Config, item: pytest.Item
) -> Mapping[str, Callable[[], asyncio.AbstractEventLoop]]:
    return {
        "hass": policy.new_event_loop
    }

With that I quickly encountered two issues:

  • asyncio.Runner doesn't call set_event_loop for custom loop factories
  • The pytest parametrize design doesn't really seem to work for session fixtures as the loop is closed before the finalizer is called, thus causing a crash.

Comment on lines +939 to +942
runner = Runner(
debug=debug_mode,
loop_factory=_asyncio_loop_factory,
).__enter__()
Copy link
Copy Markdown
Contributor

@cdce8p cdce8p Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asyncio.Runner only sets the event loop if loop_factory=None. Some tests or fixtures might be run in a sync context but depend on the event loop still being set so it's accessible via asyncio.get_event_loop().

I'd suggest to add something along the lines of

if _asyncio_loop_factory is not None:
    asyncio.set_event_loop(runner.get_loop())

The reset is already handled by the _temporary_event_loop_policy context manager.

--
This might be specific to Python 3.14, couldn't reproduce it in 3.13

# conftest.py
import asyncio

import pytest_asyncio


def pytest_asyncio_loop_factories(config, item):
    return {
        "asyncio": asyncio.new_event_loop,
    }


@pytest_asyncio.fixture(autouse=True)
def enable_event_loop_debug() -> None:
    asyncio.get_event_loop().set_debug(True)
# test_file.py

async def test_some_function():
    assert 2 == 2
pytest_asyncio/plugin.py:506: in setup
    return super().setup()
           ^^^^^^^^^^^^^^^
pytest_asyncio/plugin.py:860: in pytest_fixture_setup
    hook_result = yield
                  ^^^^^
test_folder/conftest.py:14: in enable_event_loop_debug
    asyncio.get_event_loop().set_debug(True)
    ^^^^^^^^^^^^^^^^^^^^^^^^
/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/events.py:715: in get_event_loop
    raise RuntimeError('There is no current event loop in thread %r.'
E   RuntimeError: There is no current event loop in thread 'MainThread'.

Comment on lines +715 to +721
metafunc.parametrize(
_asyncio_loop_factory.__name__,
effective_factories.values(),
ids=effective_factories.keys(),
indirect=True,
scope=loop_scope,
)
Copy link
Copy Markdown
Contributor

@cdce8p cdce8p Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parametrization doesn't really work for test fixtures with session scope as the runner is exited before the finalizer is called, thus throwing an exception.

# conftest.py
import asyncio
import pytest_asyncio


def pytest_asyncio_loop_factories(config, item):
    return {
        "asyncio": asyncio.new_event_loop,
    }


@pytest_asyncio.fixture(autouse=True, scope="session", loop_scope="session")
async def some_fixture():
    print("Start")
    yield
    print("Done")
# test_file.py
async def test_some_function():
    assert 2 == 2

--

pytest_asyncio/plugin.py:378: in finalizer
    runner.run(async_finalizer(), context=context)
/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/runners.py:94: in run
    self._lazy_init()
/Library/Frameworks/Python.framework/Versions/3.14/lib/python3.14/asyncio/runners.py:142: in _lazy_init
    raise RuntimeError("Runner is closed")
E   RuntimeError: Runner is closed

@tjkuson
Copy link
Copy Markdown
Contributor Author

tjkuson commented Mar 29, 2026

Thanks for the thoughtful review @cdce8p! I really appreciate it. Here's a PR that I think fixes both of these issues: #1387

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

event_loop_policy for single test

4 participants