-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Component: eventkit.ops.transform.Map
Summary
The Map operator currently only supports functions that return a synchronous value or a coroutine. It incorrectly fails to handle functions that return an asyncio.Future or asyncio.Task object. This is a regression from a previous, more robust implementation.
Problem Description
The Map.on_source method contains the following logic:
obj = self._func(*args)
if asyncio.iscoroutine(obj):
# It's a coroutine, so create a task to run it
self._create_task(obj)
else:
# It's a regular synchronous value
self.emit(obj)This code explicitly checks for coroutines. If the mapped function returns a Future object, iscoroutine() returns False, and the code proceeds to the else block, incorrectly emitting the Future object itself instead of awaiting its result.
This behavior is inconsistent with other parts of the library (e.g., Event.connect, Event.wait) which correctly handle any awaitable object (Future, Task, or coroutine).
Root Cause Analysis
A git log investigation revealed that this is a regression introduced in commit 2ae8bc8 ("Cleanup coding style for readability").
The purpose of the commit was to modernize the API by replacing asyncio.ensure_future() with the newer asyncio.create_task().
The following change was made:
# In Map.on_source
- if hasattr(obj, "__await__"):
+ if asyncio.iscoroutine(obj):
# In Map._create_task
- task = asyncio.ensure_future(coro, loop=loop)
+ task = asyncio.create_task(coro)The original code was correct:
- It used
hasattr(obj, "__await__")to correctly identify any awaitable object. - It used
asyncio.ensure_future()to robustly handle both coroutines andFutures.
The refactoring to asyncio.create_task() required narrowing the check to asyncio.iscoroutine(), because create_task does not accept Future objects. This inadvertently removed support for Futures and Tasks.
The existing test suite did not have coverage for a Map function returning a Future, which is why this regression was not caught by CI.
Proposed Solution
To fix this regression and restore the correct, more flexible behavior, the Map operator should be reverted to its original logic.
-
Broaden the input check: In
Map.on_source, the condition should be changed back to check for any awaitable.# from if asyncio.iscoroutine(obj): # to if hasattr(obj, "__await__"):
-
Use a robust task creation function: In
Map._create_task, the task creation should be changed back to useasyncio.ensure_future()to correctly handle all awaitable types. The argument should also be renamed for clarity.# from def _create_task(self, coro): ... task = loop.create_task(coro) # to def _create_task(self, awaitable): ... task = asyncio.ensure_future(awaitable, loop=loop)
This change will make the Map operator more powerful, intuitive, and consistent with the rest of the eventkit library, fixing a clear regression in functionality.