Skip to content

Commit ed8c9ce

Browse files
authored
fix(rtc): avoid deprecated asyncio.iscoroutinefunction in EventEmitter.on (#815)
1 parent 8b0423f commit ed8c9ce

2 files changed

Lines changed: 97 additions & 3 deletions

File tree

‎livekit-rtc/livekit/rtc/event_emitter.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
import inspect
21
import asyncio
2+
import inspect
33
from typing import Any, Callable, Dict, Set, Optional, Generic, TypeVar
44

55
from .log import logger
66

7+
# `asyncio.iscoroutinefunction` also treats a callable tagged with this private
8+
# sentinel as a coroutine function (cpython/Lib/asyncio/coroutines.py:30-32);
9+
# `inspect.iscoroutinefunction` never has.
10+
# `@asyncio.coroutine` (removed in 3.11) applied the tag, but the sentinel itself
11+
# still exists on every version this package supports, so the check is kept on all
12+
# of them. Read defensively: it is private and may disappear with the deprecated
13+
# function in 3.16, and a bare attribute access would then fail at import time.
14+
_ASYNCIO_COROUTINE_MARKER = getattr(asyncio.coroutines, "_is_coroutine", None)
15+
716
T_contra = TypeVar("T_contra", contravariant=True)
817

918

@@ -157,7 +166,10 @@ def greet(name):
157166
```
158167
"""
159168
if callback is not None:
160-
if asyncio.iscoroutinefunction(callback):
169+
if inspect.iscoroutinefunction(callback) or (
170+
_ASYNCIO_COROUTINE_MARKER is not None
171+
and getattr(callback, "_is_coroutine", None) is _ASYNCIO_COROUTINE_MARKER
172+
):
161173
raise ValueError(
162174
"Cannot register an async callback with `.on()`. Use `asyncio.create_task` within your synchronous callback instead."
163175
)

‎tests/rtc/test_emitter.py‎

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
from livekit.rtc import EventEmitter
1+
import asyncio
2+
import functools
3+
import warnings
24
from typing import Any, Literal
5+
from unittest.mock import AsyncMock
6+
37
import pytest
48

9+
from livekit.rtc import EventEmitter
10+
511

612
def test_events() -> None:
713
EventTypes = Literal["connected", "reconnected", "disconnected"]
@@ -102,3 +108,79 @@ def on_error_another() -> None:
102108
emitter.emit("error")
103109

104110
assert len(calls) == 2
111+
112+
113+
def test_on_does_not_warn() -> None:
114+
"""Registering a callback must not emit a DeprecationWarning.
115+
116+
`asyncio.iscoroutinefunction` is deprecated in Python 3.14 and slated for removal
117+
in 3.16; `inspect.iscoroutinefunction` is the supported replacement.
118+
"""
119+
EventTypes = Literal["connected"]
120+
121+
emitter = EventEmitter[EventTypes]()
122+
123+
with warnings.catch_warnings():
124+
warnings.simplefilter("error", DeprecationWarning)
125+
126+
@emitter.on("connected")
127+
def on_connected() -> None:
128+
pass
129+
130+
emitter.once("connected", on_connected)
131+
132+
emitter.emit("connected")
133+
134+
135+
def test_on_rejects_async_callback() -> None:
136+
"""`.on()` refuses coroutine functions, however they are spelled."""
137+
EventTypes = Literal["connected"]
138+
139+
emitter = EventEmitter[EventTypes]()
140+
141+
async def on_connected() -> None:
142+
pass
143+
144+
with pytest.raises(ValueError, match="Cannot register an async callback"):
145+
emitter.on("connected", on_connected)
146+
147+
with pytest.raises(ValueError, match="Cannot register an async callback"):
148+
emitter.on("connected", functools.partial(on_connected))
149+
150+
151+
def test_on_rejects_sentinel_tagged_callback() -> None:
152+
"""A callable tagged with asyncio's private coroutine sentinel is rejected.
153+
154+
`asyncio.iscoroutinefunction` honours this tag on every supported version;
155+
`inspect.iscoroutinefunction` never has. `@asyncio.coroutine` (removed in
156+
3.11) applied it, and `unittest.mock.AsyncMock` still does.
157+
"""
158+
marker = getattr(asyncio.coroutines, "_is_coroutine", None)
159+
if marker is None:
160+
pytest.skip("asyncio.coroutines._is_coroutine is not defined on this Python")
161+
162+
EventTypes = Literal["connected"]
163+
164+
emitter = EventEmitter[EventTypes]()
165+
166+
def on_connected() -> None:
167+
pass
168+
169+
on_connected._is_coroutine = marker # type: ignore[attr-defined]
170+
171+
with pytest.raises(ValueError, match="Cannot register an async callback"):
172+
emitter.on("connected", on_connected)
173+
174+
175+
def test_on_rejects_async_mock() -> None:
176+
"""`AsyncMock` is rejected on every supported version.
177+
178+
On Python 3.9, `inspect.iscoroutinefunction(AsyncMock())` is False; only the
179+
sentinel check catches it.
180+
"""
181+
EventTypes = Literal["connected"]
182+
183+
emitter = EventEmitter[EventTypes]()
184+
185+
with pytest.raises(ValueError, match="Cannot register an async callback"):
186+
emitter.on("connected", AsyncMock())

0 commit comments

Comments
 (0)