cadrumo.core.process_binding module

One process-wide binding with a context-scoped override.

A ContextVar is the right shape for a value that must stay private to one span of work, and the wrong shape for a value that names what is true of the whole process. The live profile session is the second kind: exactly one profile is logged in per process, and every entry point – a parsed CLI invocation, a Textual thread worker, a second asyncio.run() frame in the same program – must observe the same answer.

PEP 567 semantics defeat that on their own. asyncio.run runs its loop in a fresh context, a thread does not inherit its spawner’s context, and contextvars.Context.run() discards every write made inside it. A binding installed in any of those frames is therefore invisible to the frame that reads it, which presents to the operator as a correct login followed by a surface that cannot find the profile it just authenticated.

This binding composes both needs. The process-wide value is guarded by a lock and is what an unscoped bind publishes; the context-scoped override is what a with block installs for the duration of one span and unwinds on exit. Reads consult the override first, so a nested span still shadows the process value exactly as a plain ContextVar did, and fall back to the process value when no override is in force.

The binding holds a reference, never a copy: an owner that zeroises key material on close keeps that responsibility, and this type only decides who can see the object.

final class ProcessScopedBinding(name)[source]

Bases: Generic

Publish one value process-wide while allowing a scoped override.

The override is stored as a one-tuple rather than the bare value so that “no override in this context” and “an override deliberately set to None” stay distinguishable. The second is a real state: a caller that suspends the binding for a block is asserting an absence, and collapsing it into the first would let the process value show through the very block that hid it.

Parameters:

name (str)

get()[source]

Return the override in force for this context, else the process value.

Return type:

T | None

bind(value)[source]

Publish value process-wide, and into any override in force here.

Refreshing an override in force is what keeps an unscoped bind truthful to its own caller: a bind performed inside a span that shadows the process value would otherwise be published to every context except the one that performed it.

Return type:

None

Parameters:

value (T | None)

clear_bound(expected)[source]

Remove expected wherever this context can still observe it.

A retiring owner must unbind the exact object it observed, from the override in force here as well as from the process value, because a session closed inside an overriding block is bound in both places and clearing only one leaves a zeroised object advertised as live.

Identity, not equality: a replacement installed reentrantly during the retirement is a different object and must survive it, which is exactly what an equality comparison would fail to preserve.

Return type:

None

Parameters:

expected (T)

override(value)[source]

Shadow the process value with value for the duration of the block.

Overrides stack and unwind through the Token the override variable returns, so a nested block restores the exact binding its caller observed. The process value is never touched here; only bind() and clear_bound() change what the rest of the program sees.

Return type:

Generator[None]

Parameters:

value (T | None)