todays python “outer” scopes (global, “nonlocal” and thread.locals()) have a gap: call stack specific scope.
I propose to provide an enclosing dynamic outer scope which can be accessed from the nested function, and I propose to tie this to the try ... finally / with ... / … stack unwind boundaries.
with this in place, you can easily and dependably ask for “the current transaction” or “the decimal formatting in this call stack / generator stack / async stack” or whichever bit of information you need to share in a specific call stack, given coroutines and generators, without having to pass it around explicitly.
it’s similar to “dynamic” scope but explicit in both set-up and access, and thus avoids surprises while being sufficiently efficient.
What this needs is a way to tie a (chained) map to the (virtual) block stack (now encoded in the exception table).
when the interpreter sets up a exception/* stack unwind boundary, it conceptually “captures” this context, and only that.
when the context is queried, the (virtual) dynamic block stack is walked until the context is found.
of course “unwinding” the exception stack (without unwinding it) is expensive, thus the context is a chained map alike and it can, once found, be replicated to the top of the stack.
has anything like this ever been discussed? where should I discuss it, if not here?
Not entirely sure about what exactly you mean with “generators”, but contextvars do 100% work with coroutines and can be temporarily changed for the current stack. Can you share an example of where you want want to use this local scope you are proposing where contextvars aren’t enough?
first, thanks for entering the sparring, this really helps me to clearly formulate my intent
and technically you’re correct indeed, contextvars somehwat “work” with generators. but either I’m doing it wrong or heck it’s unintuitive.
and I still can’t run them in different contexts simultaneously but have to set them up artfully in different contexts and then run them simultaneously.
here is an example:
import contextvars
import dataclasses
@dataclasses.dataclass
class FooBar:
S: str
K: int
foobarconf: contextvars.ContextVar[FooBar] = contextvars.ContextVar("foobarconf", default="unset")
foobarconf.set("unset")
def foobar():
fb = foobarconf.get()
print(f"{fb=}")
yield
i = 1
while True:
print(f"{foobarconf.get()=}")
if i % fb.K == 0:
yield fb.S
else:
yield i
i += 1
def setup_foobar(fb: FooBar):
print(f"{foobarconf.get()=}")
ctx = contextvars.copy_context()
def murgh(fb):
foobarconf.set(fb)
ctx.run(murgh, fb)
print(f"{foobarconf.get()=}")
print(f"{ctx[foobarconf]=}")
print([ctx.get(i) for i in ctx])
return ctx
def fizzbuzz():
root_context = contextvars.copy_context()
print('-' * 10)
fizz_conf = FooBar("Fizz", 3)
fizz_context = root_context.run(setup_foobar, fizz_conf)
print('-' * 10)
buzz_conf = FooBar("Buzz", 5)
buzz_context = root_context.run(setup_foobar, buzz_conf)
print('-' * 10)
print([i.get() for i in fizz_context])
print([i.get() for i in buzz_context])
print('-' * 10)
fizz = foobar()
fizz_context.run(fizz.__next__)
print('-' * 10)
buzz = foobar()
buzz_context.run(buzz.__next__)
print('-' * 10)
print([fizz_context.get(i) for i in fizz_context])
print([buzz_context.get(i) for i in buzz_context])
print('-' * 10)
for T in zip(range(1, 17), fizz, buzz):
print(f"{T=}")
print('-' * 10)
if __name__ == "__main__":
fizzbuzz()
see the hoops to jump to make at least the initialization read the context?
once they’re running they are in the same context…