Skip to content

"RuntimeError: Thread 'MainThread' already has a main context" when used with glib event loop #1233

@marmarek

Description

@marmarek

glib event loop has its own event loop policy, and implicitly sets the event loop for the main thread. See https://pygobject.gnome.org/guide/asynchronous.html
Note this needs gobject >= 3.50.0, to have the native asyncio support.

This doesn't work with pytest-asyncio and results in exception as in title when pytest_fixture_setup calls policy.set_event_loop(loop).

Minimal reproducer:

import asyncio
import pytest

from gi.events import GLibEventLoopPolicy
asyncio.set_event_loop_policy(GLibEventLoopPolicy())

@pytest.mark.asyncio
async def test_foo():
    pass

The output:

======================================= ERRORS =======================================
_____________________________ ERROR at setup of test_foo _____________________________

policy = <gi.events.GLibEventLoopPolicy object at 0x7f81069ecc20>

    @contextlib.contextmanager
    def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]:
        old_loop_policy = _get_event_loop_policy()
        try:
            old_loop = _get_event_loop_no_warn()
        except RuntimeError:
            old_loop = None
        _set_event_loop_policy(policy)
        try:
>           yield

usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:543: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:757: in _scoped_runner
    runner = Runner().__enter__()
usr/lib64/python3.14/asyncio/runners.py:59: in __enter__
    self._lazy_init()
usr/lib64/python3.14/asyncio/runners.py:150: in _lazy_init
    events.set_event_loop(self._loop)
usr/lib64/python3.14/asyncio/events.py:839: in set_event_loop
    _get_event_loop_policy().set_event_loop(loop)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <gi.events.GLibEventLoopPolicy object at 0x7f81069ecc20>
loop = <GLibEventLoop running=False closed=False debug=False ctx=0x5FB5B1886F60 loop=0x5FB5B1B968A0>

    def set_event_loop(self, loop):
        """Set the event loop for the current context (python thread) to loop.
    
        This is only permitted if the thread has no thread default main context
        with the main thread using the default main context.
        """
        # Only accept glib event loops, otherwise things will just mess up
        assert loop is None or isinstance(loop, GLibEventLoop)
    
        ctx = ctx_td = GLib.MainContext.get_thread_default()
        if ctx is None and threading.current_thread() is threading.main_thread():
            ctx = GLib.MainContext.default()
    
        if loop is None:
            # We do permit unsetting the current loop/context
            old = self._loops.pop(hash(ctx), None)
            if old:
                if hash(old._context) != hash(ctx):
                    warnings.warn(
                        "GMainContext was changed unknowingly by asyncio integration!",
                        RuntimeWarning,
                    )
                if ctx_td:
                    GLib.MainContext.pop_thread_default(ctx_td)
        else:
            # Only allow attaching if the thread has no main context yet
            if ctx:
>               raise RuntimeError(
                    f"Thread {threading.current_thread().name!r} already has a main context, "
                    "get_event_loop() will create a new loop if needed"
                )
E               RuntimeError: Thread 'MainThread' already has a main context, get_event_loop() will create a new loop if needed

usr/lib64/python3.14/site-packages/gi/events.py:813: RuntimeError

During handling of the above exception, another exception occurred:

fixturedef = <FixtureDef argname='_function_scoped_runner' scope='function' baseid=''>
request = <SubRequest '_function_scoped_runner' for <Coroutine test_foo>>

    @pytest.hookimpl(wrapper=True)
    def pytest_fixture_setup(fixturedef: FixtureDef, request) -> object | None:
        asyncio_mode = _get_asyncio_mode(request.config)
        if not _is_asyncio_fixture_function(fixturedef.func):
            if asyncio_mode == Mode.STRICT:
                # Ignore async fixtures without explicit asyncio mark in strict mode
                # This applies to pytest_trio fixtures, for example
>               return (yield)

usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:681: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:756: in _scoped_runner
    with _temporary_event_loop_policy(new_loop_policy):
usr/lib64/python3.14/contextlib.py:162: in __exit__
    self.gen.throw(value)
usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:546: in _temporary_event_loop_policy
    _set_event_loop(old_loop)
usr/lib/python3.14/site-packages/pytest_asyncio/plugin.py:575: in _set_event_loop
    asyncio.set_event_loop(loop)
usr/lib64/python3.14/asyncio/events.py:839: in set_event_loop
    _get_event_loop_policy().set_event_loop(loop)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <gi.events.GLibEventLoopPolicy object at 0x7f81069ecc20>
loop = <GLibEventLoop running=False closed=False debug=False ctx=0x5FB5B1762B90 loop=0x5FB5B19B7110>

    def set_event_loop(self, loop):
        """Set the event loop for the current context (python thread) to loop.
    
        This is only permitted if the thread has no thread default main context
        with the main thread using the default main context.
        """
        # Only accept glib event loops, otherwise things will just mess up
        assert loop is None or isinstance(loop, GLibEventLoop)
    
        ctx = ctx_td = GLib.MainContext.get_thread_default()
        if ctx is None and threading.current_thread() is threading.main_thread():
            ctx = GLib.MainContext.default()
    
        if loop is None:
            # We do permit unsetting the current loop/context
            old = self._loops.pop(hash(ctx), None)
            if old:
                if hash(old._context) != hash(ctx):
                    warnings.warn(
                        "GMainContext was changed unknowingly by asyncio integration!",
                        RuntimeWarning,
                    )
                if ctx_td:
                    GLib.MainContext.pop_thread_default(ctx_td)
        else:
            # Only allow attaching if the thread has no main context yet
            if ctx:
>               raise RuntimeError(
                    f"Thread {threading.current_thread().name!r} already has a main context, "
                    "get_event_loop() will create a new loop if needed"
                )
E               RuntimeError: Thread 'MainThread' already has a main context, get_event_loop() will create a new loop if needed

usr/lib64/python3.14/site-packages/gi/events.py:813: RuntimeError
================================== warnings summary ==================================
usr/lib64/python3.14/site-packages/gi/events.py:718
  /usr/lib64/python3.14/site-packages/gi/events.py:718: DeprecationWarning: 'asyncio.AbstractEventLoopPolicy' is deprecated and slated for removal in Python 3.16
    class GLibEventLoopPolicy(asyncio.AbstractEventLoopPolicy):

testcase.py:5
  /testcase.py:5: DeprecationWarning: 'asyncio.set_event_loop_policy' is deprecated and slated for removal in Python 3.16
    asyncio.set_event_loop_policy(GLibEventLoopPolicy())

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================== short test summary info ===============================
ERROR testcase.py::test_foo - RuntimeError: Thread 'MainThread' already has a main context, get_event_loop() wi...

Non-minimal failure: https://gitlab.com/QubesOS/qubes-desktop-linux-menu/-/jobs/11350892441

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions