Eager task factory ergonomics

Love eager task factory, feels weird to use though.

The preference for set_task_factory over a start_eager kwarg to create_task puzzles me. Is an eager task factory only supposed to be used by end users? If not, where should a library set the task factory – some singleton function?

I could see the rationale that end-users should be able to control what task factory is used, but there doesn’t seem to be an “ordinary” task factory to pass to set_task_factory, which would affix it as default and signal to libraries that they shouldn’t override it – there’s only None and eager_task_factory.

Forcefully overriding the task factory in a library feels like an anti-pattern that leads to unexpected behavior for end-users; I’ve resorted to creating tasks with an ephemeral task factory like:

@contextmanager
def _get_loop():
    # careful using this, you should NOT async yield within the context
    loop = asyncio.get_running_loop()

    if sys.version_info.minor < 12:
        yield loop
        return

    # temporarily override default task factory as eager
    task_factory_bak = loop.get_task_factory()
    loop.set_task_factory(asyncio.eager_task_factory)  # type: ignore
    try:
        yield loop
    finally:
        loop.set_task_factory(task_factory_bak)


def create_task(coro: Coroutine):
    with _get_loop() as loop:
        return loop.create_task(coro)
1 Like

You’re doing a lot more work than you need to, but I’d also like to be able to toggle eagerness at asyncio.create_task

import asyncio
import typing

T = typing.TypeVar("T")

def create_task(
    coro: Coroutine[Any, Any, T], name: str | None = None, eager: bool=False
) -> asyncio.Task[T]:
    t = asyncio.Task(coro, asyncio.get_running_loop(), eager_start=eager)
    t.set_name(name)
    return t

This is slightly more verbose than calling eager_task_factory directly. Neither option composes with task groups, unfortunately, whereas OP’s does.