[Python-Dev] PEP 550 v4: coroutine policy (original) (raw)

Antoine Pitrou antoine at python.org
Tue Aug 29 15:32:35 EDT 2017


Le 29/08/2017 à 21:18, Yury Selivanov a écrit :

On Tue, Aug 29, 2017 at 2:40 PM, Antoine Pitrou <solipsis at pitrou.net> wrote:

On Mon, 28 Aug 2017 17:24:29 -0400 Yury Selivanov <yselivanov.ml at gmail.com> wrote:

Long story short, I think we need to rollback our last decision to prohibit context propagation up the call stack in coroutines. In PEP 550 v3 and earlier, the following snippet would work just fine:

var = newcontextvar() async def bar(): var.set(42) async def foo(): await bar() assert var.get() == 42 # with previous PEP 550 semantics rununtilcomplete(foo()) But it would break if a user wrapped "await bar()" with "waitfor()": var = newcontextvar() async def bar(): var.set(42) async def foo(): await waitfor(bar(), 1) assert var.get() == 42 # AssertionError !!! rununtilcomplete(foo()) [...] Why wouldn't the bar() coroutine inherit the LC at the point it's instantiated (i.e. where the synchronous bar() call is done)? We want tasks to have their own isolated contexts. When a task is started, it runs its code in parallel with its "parent" task.

I'm sorry, but I don't understand what it all means.

To pose the question differently: why is example #1 supposed to be different, philosophically, than example #2? Both spawn a coroutine, both wait for its execution to end. There is no reason that adding a wait_for() intermediary (presumably because the user wants to add a timeout) would significantly change the execution semantics of bar().

waitfor() in the above example creates an asyncio.Task implicitly, and that's why we don't see 'var' changed to '42' in foo().

I don't understand why a non-obvious behaviour detail (the fact that wait_for() creates an asyncio.Task implicitly) should translate into a fundamental difference in observable behaviour. I find it counter-intuitive and error-prone.

This is a slightly complicated case, but it's addressable with a good documentation and recommended best practices.

It would be better addressed with consistent behaviour that doesn't rely on specialist knowledge, though :-/

This means that PEP 550 will have a caveat for async code: don't rely on context propagation up the call stack, unless you are writing aenter and aexit that are guaranteed to be called without being wrapped into a Task.

Hmm, sorry for being a bit slow, but I'm not sure what this sentence implies. How is the user supposed to know whether something will be wrapped into a Task (short of being an expert in asyncio internals perhaps)? Actually, if could whip up an example of what you mean here, it would be helpful I think :-) aenter won't ever be wrapped in a task because its called by the interpreter. var = newcontextvar() class MyAsyncCM: def aenter(self): var.set(42) async with MyAsyncCM(): assert var.get() == 42 The above snippet will always work as expected.

Uh... So I really don't understand what you meant above when you wrote:

""" This means that PEP 550 will have a caveat for async code: don't rely on context propagation up the call stack, unless you are writing aenter and aexit that are guaranteed to be called without being wrapped into a Task. """

To ask the question again: can you showcase how and where the "caveat" applies?

Regards

Antoine.



More information about the Python-Dev mailing list