Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions livekit-rtc/livekit/rtc/event_emitter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import inspect
import asyncio
from typing import Any, Callable, Dict, Set, Optional, Generic, TypeVar

from .log import logger
Expand Down Expand Up @@ -157,7 +156,7 @@ def greet(name):
```
"""
if callback is not None:
if asyncio.iscoroutinefunction(callback):
if inspect.iscoroutinefunction(callback):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Legacy coroutine callbacks are accepted

On Python 3.9 and 3.10, inspect.iscoroutinefunction accepts callbacks decorated with asyncio.coroutine. emit discards their returned generators, so listeners never execute.

Learn more

The package supports Python 3.9 and 3.10, where asyncio.coroutine still creates generator-based coroutine functions. asyncio.iscoroutinefunction recognizes its _is_coroutine marker, while inspect.iscoroutinefunction does not. The new guard therefore changes behavior on supported runtimes. Registration succeeds, and emit invokes the callback synchronously without awaiting or driving the returned generator.

Example: On Python 3.10, a listener defined with @asyncio.coroutine previously raises ValueError during on. After this change, registration succeeds, but emitting the event never runs the listener body.

Recommended fix: Preserve _is_coroutine marker recognition without calling the deprecated API on Python 3.14. Add a regression test using @asyncio.coroutine on runtimes that provide it, or an equivalently marked callable.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

raise ValueError(
"Cannot register an async callback with `.on()`. Use `asyncio.create_task` within your synchronous callback instead."
)
Expand Down
40 changes: 40 additions & 0 deletions tests/rtc/test_emitter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import functools
from livekit.rtc import EventEmitter
from typing import Any, Literal
import warnings
import pytest


Expand Down Expand Up @@ -102,3 +104,41 @@ def on_error_another() -> None:
emitter.emit("error")

assert len(calls) == 2


def test_on_does_not_warn() -> None:
"""Registering a callback must not emit a DeprecationWarning.

`asyncio.iscoroutinefunction` is deprecated in Python 3.14 and slated for removal
in 3.16; `inspect.iscoroutinefunction` is the supported replacement.
"""
EventTypes = Literal["connected"]

emitter = EventEmitter[EventTypes]()

with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)

@emitter.on("connected")
def on_connected() -> None:
pass

emitter.once("connected", on_connected)

emitter.emit("connected")


def test_on_rejects_async_callback() -> None:
"""`.on()` refuses coroutine functions, however they are spelled."""
EventTypes = Literal["connected"]

emitter = EventEmitter[EventTypes]()

async def on_connected() -> None:
pass

with pytest.raises(ValueError, match="Cannot register an async callback"):
emitter.on("connected", on_connected)

with pytest.raises(ValueError, match="Cannot register an async callback"):
emitter.on("connected", functools.partial(on_connected))
Loading