Skip to content

Commit ce1843d

Browse files
fix(async): close awaitables require_await declines to await
require_await raised before awaiting, so the awaitable handed to it was never awaited and never closed. async_generator_to_sync calls it as require_await(async_gen.__anext__()), which meant the orphaned coroutine was created inside the library and no caller could reach it to clean up. CPython emitted "RuntimeWarning: coroutine method 'asend' ... was never awaited" while finalizing the async generator; the pytest config's filterwarnings = error turned that into an exception inside __del__, surfacing as an unraisable that failed the 3.13 and 3.14 jobs. require_await now takes ownership of the awaitable either way, closing it before raising when inactive. This matches maybe_submit, which hands the awaitable back to the caller when inactive rather than dropping it. Use getattr for close since Awaitable does not guarantee it. test_require_await_outside_context papered over this with a manual close(); it now asserts the coroutine was closed, which fails without the fix on every supported version rather than only on 3.13+. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 76f5114 commit ce1843d

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

src/pluggy/_async.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,22 @@ def maybe_submit(self, coro: Awaitable[_T]) -> _T | Awaitable[_T]:
6767
return coro
6868

6969
def require_await(self, coro: Awaitable[_T]) -> _T:
70-
"""Await an awaitable, raising an error if not in async context."""
70+
"""Await an awaitable, raising an error if not in async context.
71+
72+
Ownership of ``coro`` is taken either way: when inactive it is closed
73+
before raising, so callers never leak a coroutine that would later
74+
warn about never having been awaited.
75+
"""
7176
active = self._active_submitter
7277
if active is not None:
7378
res: _T = active.switch(coro)
7479
return res
7580
else:
81+
# Not going to await it, so dispose of it rather than let it be
82+
# collected unawaited (a RuntimeWarning raised inside __del__).
83+
close = getattr(coro, "close", None)
84+
if close is not None:
85+
close()
7686
raise RuntimeError("require_await called outside of async context")
7787

7888
async def run(self, sync_func: Callable[[], _T]) -> _T:

testing/test_async.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections.abc import AsyncGenerator
99
from collections.abc import Awaitable
1010
from collections.abc import Coroutine
11+
import inspect
1112
from typing import Any
1213
from typing import cast
1314

@@ -179,7 +180,10 @@ async def coro() -> None:
179180
awaitable = coro()
180181
with pytest.raises(RuntimeError, match="outside of async context"):
181182
submitter.require_await(awaitable)
182-
awaitable.close()
183+
# require_await takes ownership either way: an awaitable it declines to
184+
# await is closed, not left to warn about never being awaited when
185+
# collected.
186+
assert inspect.getcoroutinestate(awaitable) == inspect.CORO_CLOSED
183187

184188

185189
def test_maybe_submit_outside_context_returns_awaitable() -> None:

0 commit comments

Comments
 (0)