'unwind' scope to share call stack specific "context" state

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?

1 Like

Isn’t this completely covered by contextvars?

that’s what I thought until I figured contextvars is not for generators, because those are thread-local, not call-stack local.

The contextvars PEP is very explicit about this: threading.local(). it’s thread-local and not for coroutines.

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 :slight_smile:

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…