fix(rtc): make EventEmitter.once callbacks honor off() and argument trimming - #834
RaphaelFakhri wants to merge 2 commits into
Conversation
| if getattr(registered, "__wrapped__", None) == callback: | ||
| del handlers[registered] |
There was a problem hiding this comment.
🟡 Decorated listener lost during cancellation
If a listener wraps the return value of once, functools.wraps copies _once_of onto it. off then removes that listener with the original callback, even though it was registered separately through on.
Learn more
once returns its registered wrapper, and functools.wraps copies that wrapper's attribute dictionary into another function. The new function therefore inherits _once_of even though it was registered through on. When off searches by this attribute, it removes both functions.
Example: Register wrapped = emitter.once('event', original), then register @functools.wraps(wrapped) as an on('event') listener. Calling emitter.off('event', original) removes both registrations; the on listener no longer receives events.
Recommended fix: Track once registrations outside the callback's copyable attributes, such as in emitter-owned wrapper-to-original metadata. Update the mapping when wrappers are removed and preserve support for repeated registrations and bound methods.
Was this helpful? React with 👍 or 👎 to provide feedback.
Unrelated decorated listener removed by offValid finding, fixed in the new commit. Duplicate once listener survives removalValid finding, fixed in the same commit. Generating protobuf checkThe failure is unrelated to the change. The job runs |
Summary
EventEmitter.onceregisters an internal wrapper instead of the callback you pass in. This causes two bugs thatEventEmitter.ondoes not have:off(event, callback)does not remove a callback registered withonce, so the callback still runs on the nextemit.emitpasses every argument to aoncecallback. A callback that accepts fewer positional parameters than the number of emitted arguments raisesTypeError.oncallbacks get their arguments trimmed to the parameters they accept.Changes
oncesets__wrapped__on the wrapper.inspect.signaturefollows it, soemittrims arguments to the signature of the original callback.offfalls back to matching a wrapper whose__wrapped__equals the callback, which also covers bound methods.oncestill returns the value ofon, so existing callers see no change.Testing
Added five tests to
tests/rtc/test_emitter.py:offremoves aoncecallback (function and bound method).oncetrims arguments for a callback with fewer parameters than emitted arguments, and for a callback with none.oncestill passes all arguments to a*argscallback.Without the change to
event_emitter.py, 4 of the 17 tests intest_emitter.pyfail. With the change, all 17 pass.ruff format --checkandmypyare clean for the changed files.