inspect.iscoroutine only works for "native" coros

180 views
Skip to first unread message

Justin Mayfield

unread,
Nov 8, 2016, 12:03:02 AM11/8/16
to python-tulip
I know this is documented in PEP 492 but I'm struggling to understand why the inspect module has several test functions (iscoroutine, iscoroutinefunction, isawaitable) that only work for native coros.  Comparatively the same functions in the asyncio module work for both native and generator based coros.

It was an interesting exercise for me to figure this out the hard way so it's safe to say there is peril in this distinction for at least one developer.  Furthermore, the docs for both sets of functions don't elude to these subtle but important distinctions.

Are there good reasons for this distinction going forward?

Here are some surprising (to me) tests...

Python 3.5.2 (default, Oct 14 2016, 12:54:53) 
[GCC 6.2.1 20160916 (Red Hat 6.2.1-2)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import inspect
>>> import asyncio
>>> inspect.iscoroutinefunction(asyncio.sleep)
False
>>> asyncio.iscoroutinefunction(asyncio.sleep)
True
>>> inspect.iscoroutine(asyncio.sleep(1))
False
>>> asyncio.iscoroutine(asyncio.sleep(1))
True
>>> inspect.isawaitable(asyncio.sleep(1))
256


The last test is particularly interesting;  At least it's truthy?

Yury Selivanov

unread,
Nov 8, 2016, 12:20:51 AM11/8/16
to Justin Mayfield, python-tulip
Introspection functions from the inspect module are designed for native types. Introspection functions from asyncio are designed for what asyncio treats as coroutines. Consider this example:

@asyncio.coroutine
def coro():
print(‘hello’)

‘asyncio.iscoroutinefunction' will return ‘True' for ‘coro’, because it knows how specifically asyncio wraps non-generator functions. To make ‘inspect.iscoroutinefunction’ recognize ‘coro’ as a coroutine function we would need to make ‘inspect’ aware of asyncio. But asyncio is just one user of async/await and generator-based coroutines. Twisted and Tornado have their own coroutine decorators, we can’t make inspect to recognize coroutines from them all.

Yury

Justin Mayfield

unread,
Nov 8, 2016, 12:47:35 AM11/8/16
to Yury Selivanov, python-tulip
That's quite surprising to me too, BTW.  I was focused on generator vs `async def ...` coros and hadn't even tried the legacy decorator.

Would it be possible for asyncio code to be altered in a way that produces awaitables which conform with inspect's protocol?   Ie. through code flags or some other flag that inspect looks for?  I assume a similar stance could then be taken by other async libraries.

If it's just not in the cards may I humbly suggest the docs for the inspect module include some language to help users avoid some of these idiosyncrasies?

Justin

Yury Selivanov

unread,
Nov 8, 2016, 12:58:16 AM11/8/16
to python...@googlegroups.com
On 2016-11-08 12:47 AM, Justin Mayfield wrote:

> That's quite surprising to me too, BTW. I was focused on generator vs
> `async def ...` coros and hadn't even tried the legacy decorator.
>
> Would it be possible for asyncio code to be altered in a way that produces
> awaitables which conform with inspect's protocol? Ie. through code flags
> or some other flag that inspect looks for? I assume a similar stance could
> then be taken by other async libraries.
There is no protocol here. The inspect module is simply tailored for
introspection of builtin types. If something mimics a builtin type (a
coroutine, a generator, or a simple function) inspect utils won't
recognize it.

> If it's just not in the cards may I humbly suggest the docs for the inspect
> module include some language to help users avoid some of these
> idiosyncrasies?

Yes, I think we can improve docs: better clarify what is a
coroutine/coroutine function, reference asyncio functions, add an
example. Feel free to submit a patch!

Yury

Justin Mayfield

unread,
Nov 8, 2016, 2:01:32 AM11/8/16
to Yury Selivanov, python...@googlegroups.com
Comments inline..

From types.coroutine (which is also called by asyncio.coroutine)...
211 def coroutine(func):
212     """Convert regular generator function to a coroutine."""
213 
214     if not callable(func):
215         raise TypeError('types.coroutine() expects a callable')
216 
217     if (func.__class__ is FunctionType and
218         getattr(func, '__code__', None).__class__ is CodeType):
219 
220         co_flags = func.__code__.co_flags
221 
222         # Check if 'func' is a coroutine function.
223         # (0x180 == CO_COROUTINE | CO_ITERABLE_COROUTINE)
224         if co_flags & 0x180:
225             return func
226 
227         # Check if 'func' is a generator function.
228         # (0x20 == CO_GENERATOR)
229         if co_flags & 0x20:
230             # TODO: Implement this in C.
231             co = func.__code__
232             func.__code__ = CodeType(
233                 co.co_argcount, co.co_kwonlyargcount, co.co_nlocals,
234                 co.co_stacksize,
235                 co.co_flags | 0x100,  # 0x100 == CO_ITERABLE_COROUTINE
236                 co.co_code,
237                 co.co_consts, co.co_names, co.co_varnames, co.co_filename,
238                 co.co_name, co.co_firstlineno, co.co_lnotab, co.co_freevars,
239                 co.co_cellvars)
240             return func

Line 235: Setting CO_ITERABLE_COROUTINE but not CO_ITERABLE makes inspect.awaitable(corofunc()) pass but prevents inspect.iscoroutinefunction(forofunc) from passing.  Setting the flags to (CO_ITERABLE_COROUTINE & CO_ITERABLE) in this same context actually produces passing behavior for inspect.iscoroutinefuncion.

In addition, it triggers some other diagnostic code from genobject so you get some handy messages like, "RuntimeWarning: coroutine 'sleep' was never awaited".

See (from genobject.c):
  62     /* If `gen` is a coroutine, and if it was never awaited on,
  63        issue a RuntimeWarning. */
  64     if (gen->gi_code != NULL &&
  65         ((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE &&
  66         gen->gi_frame->f_lasti == -1) {
  67         if (!error_value) {
  68             PyErr_WarnFormat(PyExc_RuntimeWarning, 1,
  69                              "coroutine '%.50S' was never awaited",
  70                              gen->gi_qualname);
  71         }
  72     }

My primary question here is about the intended difference between CO_ITERABLE_COROUTINE and CO_ITERABLE;  How perverse is a change that naively modifies line 235 of types.coroutine to set both flags?  In my limited testing it produced better behavior than stock, but I honestly can't figure out the intended differences between those flags.  

Justin Mayfield

unread,
Nov 8, 2016, 4:40:05 AM11/8/16
to Yury Selivanov, python...@googlegroups.com
Reading into these flags more, I wonder if modifications to the inspect.iscoroutine* functions as well as to genobject.c's finalizer are justified.  Specifically to check for both CO_ITERABLE_COROUTINE and CO_COROUTINE in any place where a coroutine is expected.  Note that this is an inversion of my last email's statements about setting both flags in types.coroutine.  Here is some discovery to help in the discussion...

The "New Coroutine Declaration Syntax" section of PEP 482 states, "Internally, two new code object flags were introduced: CO_COROUTINE is used to mark native coroutines (defined with new syntax). CO_ITERABLE_COROUTINE is used to make generator-based coroutines compatible with native coroutines (set by types.coroutine() function)." Along with, "When a coroutine is garbage collected, a RuntimeWarning is raised if it was never awaited on (see also Debugging Features )."

The last comment does not delineate CO_ITERABLE_COROUTINE from CO_COROUTINE, which makes me think that the genobject.c finalizer code should check for un-awaited coroutines of both types.   There are a few other places affected by this including the repr output of objects as seen by before and after repr() calls to asyncio.sleep from my previous experiment in setting both COROUTINE flags...

#stock  behavior..
   >>> repr(asyncio.sleep(1))
   '<generator object sleep at 0x7fa5ac7ee0a0>'

# hacked behavior setting both CO_ITERABLE_COROUTINE and CO_COROUTINE...
   >>> asyncio.sleep(1)
   __main__:1: RuntimeWarning: coroutine 'sleep' was never awaited
   <coroutine object sleep at 0x7f8aac4c6360>

I posit that this last behavior is desirable but should be accomplished by finding all places that check for CO_COROUTINE and have them check for CO_ITERABLE_COROUTINE too.

Lastly, the docstring from inspect.iscoroutinefunction says "Return true if the object is a coroutine function.  Coroutine functions are defined with "async def" syntax, or generators decorated with `types.coroutine`."   What I find problematic here is that types.coroutine never produces a function that is compatible with inspect.iscoroutinefunction.

My suggestion is that we use a mask of something like CO_COROUTINE_ANY defined as (CO_ITERABLE_COROUTINE & CO_COROUTINE), which can be used anywhere coroutine tests need to be performed.  I can think of only a few places where you would need to know if it is native or not and for those cases I imagine an internal check of the co_flags would suffice.  If not, perhaps a new function for inspect could cover this, such as inspect.isnativecoroutinefunction().

Justin

Yury Selivanov

unread,
Nov 8, 2016, 12:37:02 PM11/8/16
to Justin Mayfield, python-tulip
Justin,

First, thanks a lot for a detailed review of PEP 492/inspect/asyncio docs.

> On Nov 8, 2016, at 4:39 AM, Justin Mayfield <too...@gmail.com> wrote:
>
> Reading into these flags more, I wonder if modifications to the inspect.iscoroutine* functions as well as to genobject.c's finalizer are justified. Specifically to check for both CO_ITERABLE_COROUTINE and CO_COROUTINE in any place where a coroutine is expected. Note that this is an inversion of my last email's statements about setting both flags in types.coroutine. Here is some discovery to help in the discussion...
>
> The "New Coroutine Declaration Syntax" section of PEP 482 states, "Internally, two new code object flags were introduced: CO_COROUTINE is used to mark native coroutines (defined with new syntax). CO_ITERABLE_COROUTINE is used to make generator-based coroutines compatible with native coroutines (set by types.coroutine() function)." Along with, "When a coroutine is garbage collected, a RuntimeWarning is raised if it was never awaited on (see also Debugging Features ).”

This is a bug in the PEP, I’ve pushed an update: https://git.io/vXuEQ

>
> The last comment does not delineate CO_ITERABLE_COROUTINE from CO_COROUTINE, which makes me think that the genobject.c finalizer code should check for un-awaited coroutines of both types. There are a few other places affected by this including the repr output of objects as seen by before and after repr() calls to asyncio.sleep from my previous experiment in setting both COROUTINE flags...
>
> #stock behavior..
> >>> repr(asyncio.sleep(1))
> '<generator object sleep at 0x7fa5ac7ee0a0>'
>
> # hacked behavior setting both CO_ITERABLE_COROUTINE and CO_COROUTINE...
> >>> asyncio.sleep(1)
> __main__:1: RuntimeWarning: coroutine 'sleep' was never awaited
> <coroutine object sleep at 0x7f8aac4c6360>
>
> I posit that this last behavior is desirable but should be accomplished by finding all places that check for CO_COROUTINE and have them check for CO_ITERABLE_COROUTINE too.

I don’t think we should do that. CO_COROUTINE denotes that the code object will return a new distinct type - PyCoro_Type (as defined in genobject.h).

CO_ITERABLE_COROUTINE can’t exist on its own, it’s a modifier for CO_GENERATOR. Code objects with CO_ITERABLE_COROUTINE will still return *generators*, with only one extra feature: they can be awaited. We decided that we don’t want to change generators semantics too much, after all we have CoroWrapper & debug mode in asyncio to catch unawaited coroutines. We even use CoroWrapper for native coroutines, because it provides you with a more detailed stack trace.

>
> Lastly, the docstring from inspect.iscoroutinefunction says "Return true if the object is a coroutine function. Coroutine functions are defined with "async def" syntax, or generators decorated with `types.coroutine`." What I find problematic here is that types.coroutine never produces a function that is compatible with inspect.iscoroutinefunction.

This was a bug in the docstring, updated now: https://hg.python.org/cpython/rev/f7f89a4e047d. The code was actually correct, inspect.iscoroutinefunction was checking for only CO_COROUTINE flag.

>
> My suggestion is that we use a mask of something like CO_COROUTINE_ANY defined as (CO_ITERABLE_COROUTINE & CO_COROUTINE), which can be used anywhere coroutine tests need to be performed.

The flag won’t help you with Cython async-def coroutines. They implement collections.abc.Coroutine but they don’t have code objects.

I think I see your confusion now. For determining if something is an asyncio compatible coroutine you should use asyncio.iscoroutine (which is compatible with all kinds of coroutines, including ones compiled with Cython). If you want to check if an object is a Python native type - use inspect.* functions. This has to be better clarified in the docs, I agree.

Yury

Justin Mayfield

unread,
Nov 10, 2016, 6:59:44 PM11/10/16
to Yury Selivanov, python-tulip
Thank you for your thoughtful and informative reply Yury.

I'm mostly convinced that doc updates (to which you have already made several) informing users of potential peril is the safest thing to do.  So I won't harp on the issue going passed this reply.

With that being said, I am still a bit stuck on this belief that CO_ITERABLE_COROUTINE and as you now pointed out collections.abc.Coroutine types should be given the same treatment as native coros where the question being asked is, "Can this be used like a coroutine?"  From a high level perspective a user won't tend to care if the underlying implementation of a coro is generator based when asking questions about coro protocol.  They just want to know things like if they need to call `await foo()` or `foo()`.  Put another way, it is an implementation detail that some coros are generator based or otherwise (see Cython collections.abc.Coroutine subtypes).

It's my belief that by increasing the breadth of a test like inspect.iscoroutinefunction() you don't undermine the fact that some of those functions are also generator functions, you merely complement it.  One can still ask if a coro is a generator if needed and by answering yes to an object being a coroutine you aren't (or shouldn't be) altering its generator protocol.  To me that would be the deciding factor for any calls that might be extending their breadth in this regard.  For example, I ask myself, what harm comes from changing the check in `_PyGen_Finalize` to look for all types of coros, or just calling a more open minded implementation of `inspect.iscoroutine`, as a determinant for that super awesome "coroutine __ was never awaited" message?  As far as I can tell it only increases consistency and determinism by reducing variance.

Again, thank you for taking the time to walk through this with me.  I'm a huge asyncio fan and love building things with it.  Now it's time to get back to doing just that...

Cheers,
Justin

Yury Selivanov

unread,
Nov 14, 2016, 12:50:46 PM11/14/16
to Justin Mayfield, python-tulip
Hi Justin,

> On Nov 10, 2016, at 6:59 PM, Justin Mayfield <too...@gmail.com> wrote:
>
> Thank you for your thoughtful and informative reply Yury.
>
> I'm mostly convinced that doc updates (to which you have already made several) informing users of potential peril is the safest thing to do. So I won't harp on the issue going passed this reply.
>
> With that being said, I am still a bit stuck on this belief that CO_ITERABLE_COROUTINE and as you now pointed out collections.abc.Coroutine types should be given the same treatment as native coros where the question being asked is, "Can this be used like a coroutine?”

Yes, and that’s what why we have ‘inspect.isawaitable’ and ‘asyncio.iscoroutine’ functions.

We still can’t change ‘inspect.iscoroutine' to return ‘True’ for CO_ITERABLE_COROUTINE and collection.abc.Coroutine instances. Reason 1: that would break the contract for many programs that are our there right now. The following code will break (and I know such code is out there because I’ve written some similar code myself):

if inspect.iscoroutine(obj):
if obj.cr_running:
do_something(obj.cr_await)

Reason 2: is* functions in the inspect module return ‘True’ only for built-in types. We don’t use collections.abc.Generator for inspect.isgenerator(), and we don’t want to change that. Changing only inspect.iscoroutine to use collections.abc doesn’t make a lot of sense.


> From a high level perspective a user won't tend to care if the underlying implementation of a coro is generator based when asking questions about coro protocol. They just want to know things like if they need to call `await foo()` or `foo()`. Put another way, it is an implementation detail that some coros are generator based or otherwise (see Cython collections.abc.Coroutine subtypes).
>
> It's my belief that by increasing the breadth of a test like inspect.iscoroutinefunction() you don't undermine the fact that some of those functions are also generator functions, you merely complement it. One can still ask if a coro is a generator if needed and by answering yes to an object being a coroutine you aren't (or shouldn't be) altering its generator protocol. To me that would be the deciding factor for any calls that might be extending their breadth in this regard. For example, I ask myself, what harm comes from changing the check in `_PyGen_Finalize` to look for all types of coros, or just calling a more open minded implementation of `inspect.iscoroutine`, as a determinant for that super awesome "coroutine __ was never awaited" message? As far as I can tell it only increases consistency and determinism by reducing variance.

Maybe we could update _PyGen_Finalize to issue warnings for CO_ITERABLE_COROUTINE. We’ll have to make sure that the change makes sense for frameworks like curio, which uses generators decorated with types.coroutine to implement some core machinery. And the thing is, it’s too late to make it happen in Python 3.6.

Updating finalization (and inspect.iscoroutine fwiw) in 3.7 won’t affect much, because when 3.7 is released I hope almost everybody will switch to async/await (and forget about generator-based coroutines).

>
> Again, thank you for taking the time to walk through this with me. I'm a huge asyncio fan and love building things with it. Now it's time to get back to doing just that…

NP! Please continue working with asyncio and asking questions!

Yury

Reply all
Reply to author
Forward
0 new messages