pytest-dev / pytest-dev/pytest-asyncio

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

Open
#1,233 10 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

help wanted
Dominant language
Python
Stars
1.7k
Forks
207
Avg merge
5h 35m
Merged PRs (30d)
9

Description

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

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read pytest_asyncio/plugin.py around pytest_fixture_setup, _scoped_runner, and _temporary_event_loop_policy, then inspect the shown GLibEventLoopPolicy.set_event_loop path in gi/events.py. Reproduce the failure with the minimal test and gobject >= 3.50.0. Done means the reproducer runs without the main-context RuntimeError and relevant regression coverage is present.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.