fix(rtc): use inspect.iscoroutinefunction in EventEmitter.on - #814
xbt-a4224j wants to merge 1 commit into
Conversation
| """ | ||
| if callback is not None: | ||
| if asyncio.iscoroutinefunction(callback): | ||
| if inspect.iscoroutinefunction(callback): |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
Fixes #790
Problem
EventEmitter.on()callsasyncio.iscoroutinefunction()to reject coroutine callbacks. That function is deprecated in Python 3.14 and slated for removal in 3.16 (CPython gh-122858), so on 3.14 every callback registration emits aDeprecationWarning:Because
Roomand friends register many handlers, this is noisy in practice — the existingtests/rtc/test_emitter.pyalone produced 10 of them. It also becomes a hard failure in 3.16.Fix
Use
inspect.iscoroutinefunction(), which the module already imports.The two are equivalent for every input this guard sees. They originally differed only because
asyncio's variant also recognised generator-based coroutines marked by@asyncio.coroutine; that decorator was removed in Python 3.11, leaving nothing for the marker to mark. I verified equivalence rather than assuming it —async def, plaindef, lambda, sync/async__call__objects,functools.partial(single and nested), async generator functions, andinspect.markcoroutinefunctionall agree on Python 3.14.7.Compatibility is safe for this package:
inspect.iscoroutinefunctionhas existed since 3.5 and has unwrappedfunctools.partialsince 3.8, andrequires-pythonhere is>=3.9.import asynciois removed because it is now unused — the only remaining occurrence of "asyncio" in the file is inside theValueErrormessage text, not a reference. Leaving it would failruffF401.Testing
Two tests added to
tests/rtc/test_emitter.py:test_on_does_not_warn— promotesDeprecationWarningto an error around registration, so a future reintroduction fails loudly.test_on_rejects_async_callback— asserts the guard still rejects coroutine functions, including afunctools.partial-wrapped one. The guard previously had no test coverage.Verified as a real flip against the released
livekitwheel on Python 3.14.7:1 failed, 5 passed, 12 warnings6 passed, 2 warnings1 failed, 5 passedLint, run as CI runs it from the repo root — both clean, and unchanged from
main:ruff check .— All checks passed!ruff format --check .— 101 files already formattedNote: I could not run the full
livekit-rtc/testssuite locally, as those are end-to-end tests requiringLIVEKIT_URL/LIVEKIT_API_KEY/LIVEKIT_API_SECRET, and Git LFS was unavailable so the audio fixtures remained pointer files. The change is confined to a pure-Python guard with no FFI surface.