Skip to content

Add firstresult=True and wrapper=True support to AHook async dispatch#258

Open
schuyler wants to merge 2 commits intonextline-dev:mainfrom
schuyler:main
Open

Add firstresult=True and wrapper=True support to AHook async dispatch#258
schuyler wants to merge 2 commits intonextline-dev:mainfrom
schuyler:main

Conversation

@schuyler
Copy link
Copy Markdown

@schuyler schuyler commented Apr 30, 2026

Summary

Two related additions to AHook async dispatch, building on each other:

1. firstresult=True support

AHook.__getattr__ crashes on any async hook with firstresult=True_multicall returns a single coroutine, and asyncio.gather(*coro) raises TypeError. This adds _call_firstresult() to bypass _multicall and drive dispatch manually: iterate reversed(hookimpls), await each sequentially, return the first non-None result.

9 tests in tests/wrap/test_ahook_firstresult.py.

2. wrapper=True support

Adds _async_multicall() to handle wrapper=True hookimpls — sync generator wrappers that form middleware chains around async non-wrappers. Wrappers yield once, receive the aggregate result (or exception) via send/throw, and return a (possibly modified) result via StopIteration.value. This matches pluggy's protocol exactly.

AHook.__getattr__ now has three dispatch paths:

  • Wrappers present → _async_multicall (handles both firstresult modes)
  • No wrappers + firstresult=True_call_firstresult
  • No wrappers + firstresult=Falsehook() + asyncio.gather() (unchanged)

17 tests in tests/wrap/test_ahook_wrapper.py.

Test plan

  • All 26 new tests pass (tests/wrap/)
  • Existing apluggy tests unaffected
  • No wrappers present → existing dispatch paths unchanged (regression tests included)

@schuyler schuyler requested a review from TaiSakuma as a code owner April 30, 2026 21:07
apluggy's AHook dispatched all hooks through pluggy's _multicall, which
returns a single coroutine (not a list) for firstresult=True hooks.
asyncio.gather(*coro) then crashes with TypeError.

Add _call_firstresult() that bypasses _multicall: detects firstresult via
hook.spec.opts, iterates reversed(hook.get_hookimpls()) to match pluggy's
execution order (tryfirst → plain → trylast), awaits each sequentially,
and stops on the first non-None result. The firstresult=False path is
unchanged.
Add _async_multicall to drive sync-generator wrappers around async
non-wrapper hookimpls. Wrappers yield once and receive the aggregate
result (or exception) via send/throw, matching pluggy's protocol.

AHook.__getattr__ now routes through _async_multicall when any wrapper
hookimpl is present, preserving the existing gather and firstresult
fast paths for the common no-wrapper case.
@schuyler schuyler changed the title Fix AHook async dispatch for firstresult=True hooks Add firstresult=True and wrapper=True support to AHook async dispatch Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant