Skip to content

Commit dae3da2

Browse files
prepare-sync: detect & warn on async user implementations
PR-C.3 review fixup. The opt-in-via-hasattr contract means pyright doesn't catch a user signature mismatch when a developer assumes "all observer methods are async" and defines ``async def prepare_sync(...)``. Today the call silently returns an unawaited coroutine — the prep work never runs and Python emits a delayed "coroutine was never awaited" RuntimeWarning at GC time, breaking log correlation in a way that's hard to trace back to the observer. In ``_dispatch``, after each ``prepare_sync(event)`` returns, check ``inspect.isawaitable(result)``. On hit: close the awaitable (suppresses the secondary RuntimeWarning) and emit an explicit ``warnings.warn`` naming the misconfiguration so it fails loudly at the call site. Post-call detection catches the common ``async def`` case AND the rarer lambda-returning-coroutine / ``functools.partial``-of-async cases — one check, all forms covered.
1 parent 7e59ab9 commit dae3da2

1 file changed

Lines changed: 30 additions & 1 deletion

File tree

src/openarmature/graph/observer.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from __future__ import annotations
2929

3030
import asyncio
31+
import inspect
3132
import warnings
3233
from collections.abc import Iterable
3334
from dataclasses import dataclass, field
@@ -397,12 +398,40 @@ def _dispatch(context: _InvocationContext, event: NodeEvent) -> None:
397398
if prepare_sync is None:
398399
continue
399400
try:
400-
prepare_sync(event)
401+
result = prepare_sync(event)
401402
except Exception as e:
402403
warnings.warn(
403404
f"observer prepare_sync raised {type(e).__name__}: {e}",
404405
stacklevel=2,
405406
)
407+
continue
408+
if inspect.isawaitable(result):
409+
# ``prepare_sync`` is opt-in via ``hasattr`` (not a
410+
# Protocol method) so pyright can't catch a user's
411+
# ``async def prepare_sync`` signature drift up front.
412+
# The call here would silently return an unawaited
413+
# coroutine — the prep work wouldn't run AND Python
414+
# would emit a delayed "coroutine was never awaited"
415+
# warning at GC time. Close the awaitable to suppress
416+
# that secondary noise and surface the misconfiguration
417+
# via our own explicit warn so it fails loudly at the
418+
# call site. ``getattr`` rather than ``hasattr``+method
419+
# access keeps pyright's strict-mode happy on the
420+
# ``Awaitable`` type (``.close`` lives on
421+
# ``Coroutine``, not the broader ``Awaitable``).
422+
close_method = getattr(result, "close", None)
423+
if close_method is not None:
424+
try:
425+
close_method()
426+
except Exception:
427+
pass
428+
warnings.warn(
429+
"observer prepare_sync returned an awaitable; "
430+
"prepare_sync MUST be sync (define as `def`, not "
431+
"`async def`). The synchronous prep work did NOT "
432+
"run; log correlation will miss this node's span.",
433+
stacklevel=2,
434+
)
406435
context.queue.put_nowait(_QueuedItem(event=event, observers=observers))
407436

408437

0 commit comments

Comments
 (0)