Skip to content

Async client broken on Python 3.12+: event_global_loop causes cross-loop RuntimeError #169

Description

@RichardAtCT

Bug Description

The async client (NewAClient) is broken on Python 3.12+ due to event_global_loop being a separate event loop from the one created by asyncio.run(). This causes a RuntimeError: Task got Future attached to a different loop when calling await client.idle().

Steps to Reproduce

import asyncio
from neonize.aioze.client import NewAClient
from neonize.aioze.events import ConnectedEv

async def main():
    client = NewAClient("test_bot")

    @client.event(ConnectedEv)
    async def on_connected(client, event):
        print("Connected!")

    await client.connect()
    await client.idle()  # RuntimeError here

asyncio.run(main())

Error:

RuntimeError: Task <Task pending ...> got Future <Task pending coro=<to_thread() ...>> attached to a different loop

Root Cause

In neonize/aioze/events.py line 52:

event_global_loop = asyncio.new_event_loop()

This creates a separate event loop at import time. Then in NewAClient.__init__:

self.loop = event_global_loop

And in connect():

self.connect_task = self.loop.create_task(task)  # task created on event_global_loop

When idle() does await self.connect_task, it tries to await a task from event_global_loop while running on the loop created by asyncio.run() — which raises RuntimeError on Python 3.12+.

Additionally, __onQr and Event.execute use asyncio.run_coroutine_threadsafe(..., event_global_loop) to schedule callbacks. Since event_global_loop is never started (no run_forever()), these coroutines are scheduled but never executed — so QR codes never display and event handlers never fire.

Suggested Fix

In connect(), use the running loop instead of event_global_loop:

async def connect(self):
    self.loop = asyncio.get_running_loop()
    # ... rest of connect

And update execute() and __onQr to reference self.loop (or the module-level variable) consistently, ensuring it points to the running loop.

Current Workaround

Patch both module-level references before creating the client:

import neonize.aioze.client as neonize_client
import neonize.aioze.events as neonize_events

loop = asyncio.get_running_loop()
neonize_events.event_global_loop = loop
neonize_client.event_global_loop = loop

client = NewAClient("bot")
client.loop = loop

Both modules must be patched because client.py imports its own copy via from .events import event_global_loop.

Secondary Issue: QR handler dispatch

@client.event(QREv) registers in list_func, but QR events are dispatched through event._qr via __onQr, not through execute(). Users must use @client.qr instead, which isn't obvious from the API.

Environment

  • neonize 0.3.14.post0
  • Python 3.13
  • macOS (Apple Silicon)

Happy to submit a PR with the fix if you'd like.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions