Skip to content

Map Operator Fails to Support Future Objects Due to Regression #10

@gnzsnz

Description

@gnzsnz

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:

  1. It used hasattr(obj, "__await__") to correctly identify any awaitable object.
  2. It used asyncio.ensure_future() to robustly handle both coroutines and Futures.

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.

  1. 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__"):
  2. Use a robust task creation function: In Map._create_task, the task creation should be changed back to use asyncio.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions