Potential Issue: Overly restrictive coroutine check rejects valid Cython awaitables #1105
Replies: 3 comments
|
Your reading looks right to me, and there's a detail in the source that supports it: the guard is marked as never-executed. coro = self.trace_extension(prefix_and_name, info)
if not inspect.iscoroutine(coro): # pragma: no cover
raise TypeError(
"If you're using an asynchronous interface, "
"the callback of the `trace` extension should "
"be an asynchronous function rather than a normal function."
)
await coroThat The argument for It also doesn't weaken the guard's actual purpose. The mistake the message describes — passing a normal function — returns whatever that function returned, typically Worth mentioning the second symptom in your report as part of the same change: the raise happens after the callback has already been called, so the returned awaitable is discarded without ever being awaited, which is where the |
|
Your reading is right, and I think the fix is safe for a reason beyond the awaitable protocol itself.
The current code is: coro = self.trace_extension(prefix_and_name, info)
if not inspect.iscoroutine(coro): # pragma: no cover
raise TypeError(
"If you're using an asynchronous interface, "
"the callback of the `trace` extension should "
"be an asynchronous function rather than a normal function."
)
await coroSwitching to The One nit on the error message if you do open a PR. If it is going to accept any awaitable, then wording it around "asynchronous function" is slightly off, since a callable returning an awaitable is now fine too. Something like "should return an awaitable" would match the new check more closely. Worth checking whether |
|
The
ret = self.trace_extension(prefix_and_name, info)
if inspect.iscoroutine(ret): # pragma: no cover
raise TypeError(
"If you are using a synchronous interface, "
"the callback of the `trace` extension should "
"be a normal function instead of an asynchronous function."
)That guard exists to catch someone handing an async callback to the sync interface. It uses the same predicate, so it inherits the same blind spot — but the consequence is inverted. Your Cython callback returns something where So the fix goes the same direction in both places, which is a bit counterintuitive given they're checking for opposite mistakes: # trace() — widen, so Cython async callbacks are caught instead of ignored
if inspect.isawaitable(ret):
raise TypeError(...)
# atrace() — widen, so Cython async callbacks are accepted instead of rejected
if not inspect.isawaitable(coro):
raise TypeError(...)Both still do what the error messages promise. A plain function returning Why One thing to fold in while touching these lines: in both branches the callback has already been invoked when the if inspect.isawaitable(ret):
if hasattr(ret, "close"):
ret.close()
raise TypeError(...)Both branches carry class FakeAwaitable:
def __await__(self):
yield from ()
return NoneThat covers both branches and pins the behaviour without adding Cython to the test matrix. |
Uh oh!
There was an error while loading. Please reload this page.
I created a minimal reproducer, but the issue template asks that potential bugs start as discussions. httpcore’s coroutine check rejects valid awaitables returned by Cython-compiled async callbacks.
Environment
Python: 3.14.3
httpcore: 1.0.9
Cython: 3.2.9
setuptools: 80.9.0
OS: Linux x86_64
Expected behavior
The Cython-compiled callback should be accepted because its result implements the awaitable protocol and can be used with await.
Actual behavior
inspect.isawaitable: True
inspect.iscoroutine: False
TypeError: If you're using an asynchronous interface, the callback of the
traceextension should be an asynchronous function rather than a normalfunction.
RuntimeWarning: coroutine 'trace_callback' was never awaited
The pure-Python invocation reports:
inspect.isawaitable: True
inspect.iscoroutine: True
and completes successfully.
Likely cause
Trace.atrace()currently validates the returned object with:Because the object is immediately used with await, would it be appropriate to validate the general awaitable protocol instead?
Demonstration
repro.py
How to run it
All reactions