EDIT: As pointed out by Thierry Lathuille, PEP567, where ContextVar
was introduced, was not designed to address generators (unlike the withdrawn PEP550). Still, the main question remains. How do I write stateful context managers that behave correctly with multiple threads, generators and asyncio
tasks?
I have a library with some functions that can work in different "modes", so their behavior can be altered by a local context. I am looking at the contextvars
module to implement this reliably, so I can use it from different threads, asynchronous contexts, etc. However, I am having trouble getting a simple example working right. Consider this minimal setup:
from contextlib import contextmanager
from contextvars import ContextVar
MODE = ContextVar('mode', default=0)
@contextmanager
def use_mode(mode):
t = MODE.set(mode)
try:
yield
finally:
MODE.reset(t)
def print_mode():
print(f'Mode {MODE.get()}')
Here is a small test with a generator function:
def first():
print('Start first')
print_mode()
with use_mode(1):
print('In first: with use_mode(1)')
print('In first: start second')
it = second()
next(it)
print('In first: back from second')
print_mode()
print('In first: continue second')
next(it, None)
print('In first: finish')
def second():
print('Start second')
print_mode()
with use_mode(2):
print('In second: with use_mode(2)')
print('In second: yield')
yield
print('In second: continue')
print_mode()
print('In second: finish')
first()
I get the following output:
Start first
Mode 0
In first: with use_mode(1)
In first: start second
Start second
Mode 1
In second: with use_mode(2)
In second: yield
In first: back from second
Mode 2
In first: continue second
In second: continue
Mode 2
In second: finish
In first: finish
In the section:
In first: back from second
Mode 2
In first: continue second
It should be Mode 1
instead of Mode 2
, because this was printed from first
, where the applying context should be, as I understand it, use_mode(1)
. However, it seems that the use_mode(2)
of second
is stacked over it until the generator finishes. Are generators not supported by contextvars
? If so, is there any way to support stateful context managers reliably? By reliably, I mean it should behave consistently whether I use:
- Multiple threads.
- Generators.
asyncio