Implement event loop factory hook#1373
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
a8594aa to
d3c835d
Compare
seifertm
left a comment
There was a problem hiding this comment.
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.
30a550a to
85ce9f6
Compare
cdce8p
left a comment
There was a problem hiding this comment.
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.Runnerdoesn't callset_event_loopfor 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.
| runner = Runner( | ||
| debug=debug_mode, | ||
| loop_factory=_asyncio_loop_factory, | ||
| ).__enter__() |
There was a problem hiding this comment.
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 == 2pytest_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'.| metafunc.parametrize( | ||
| _asyncio_loop_factory.__name__, | ||
| effective_factories.values(), | ||
| ids=effective_factories.keys(), | ||
| indirect=True, | ||
| scope=loop_scope, | ||
| ) |
There was a problem hiding this comment.
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
Added the
pytest_asyncio_loop_factorieshook to parametrize asyncio tests with custom event loop factories. Users can use theloop_factoriesargument to select a subset of hooks. If omitted, the test will run parametrized with each loop factory item returned by the hook.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.