Skip to content

base

base #

The shared base for every recording context.

A [ProcedureContext][] owns two things: the ordered list of nodes emitted into it (every node constructed while it is active) and the set of node ids caught by it (used as some node's child). The active context lives in a single [ContextVar][contextvars.ContextVar]; a node registers itself through [_collect][] from Procedure.__post_init__.

ProcedureContext #

Collects the nodes a block constructs and reduces them to its roots.

Subclasses differ only in [_finish][] — how they wrap the leftover roots (a sequence, a set, an optional, a conditional) once the block exits.

Source code in capturegraph-lib/capturegraph/procedures/procedure/authoring/contexts/base.py
class ProcedureContext:
    """Collects the nodes a block constructs and reduces them to its roots.

    Subclasses differ only in [_finish][] — how they wrap the leftover roots
    (a sequence, a set, an optional, a conditional) once the block exits.
    """

    _active: ClassVar[ContextVar[ProcedureContext | None]] = ContextVar(
        "procedure_context",
        default=None,
    )

    def __init__(self) -> None:
        """Start with no parent, nothing emitted, and nothing caught."""
        self._parent: ProcedureContext | None = None
        self._emitted: list[Procedure[CGType]] = []
        self._caught: set[str] = set()

    ### Active-context access ###

    @classmethod
    def current(cls) -> ProcedureContext:
        """The active context, or raise if a node is built with none."""
        context = cls._active.get()
        if context is None:
            raise AuthoringError(
                "a procedure node was constructed with no active context — author "
                "it inside @cg.procedure, or wrap standalone construction in "
                "cg.detached()"
            )
        return context

    @classmethod
    def current_or_none(cls) -> ProcedureContext | None:
        """The active context, or ``None`` if there is none."""
        return cls._active.get()

    ### Collection (driven by Procedure.__post_init__) ###

    def _collect(self, node: Procedure[CGType]) -> None:
        """Register a freshly constructed ``node``: emit it, catch its children."""
        self._emit(node)
        for child in node._inputs.values():
            self._catch(child)
        for child in node._substeps:
            self._catch(child)

    def _emit(self, node: Procedure[CGType]) -> None:
        """Record ``node`` as constructed in this context, in order."""
        self._emitted.append(node)

    def _catch(self, node: Procedure[CGType]) -> None:
        """Mark ``node`` consumed — in this context and every ancestor.

        Walking the parent chain handles a node emitted in an outer context but
        used only inside a nested one: it is still caught in the outer context, so
        it is not flagged as uncaught data there.
        """
        uuid = node.uuid
        context: ProcedureContext | None = self
        while context is not None:
            context._caught.add(uuid)
            context = context._parent

    ### Reduction to roots ###

    def _step_roots(self) -> list[Procedure[CGType]]:
        """The emitted nodes never caught, in construction order.

        Each must be a side effect (``Void``); a leftover navigation ``Path`` node
        is a no-op and is dropped; anything else is dangling data and raises.
        """
        from capturegraph.types.special.path import (
            Path,
        )
        from capturegraph.types.special.void import (
            Void,
        )

        steps: list[Procedure[CGType]] = []
        for node in self._emitted:
            if node.uuid in self._caught:
                continue
            return_type = node.return_type
            if isinstance(return_type, type) and issubclass(return_type, Void):
                steps.append(node)
            elif isinstance(return_type, type) and issubclass(return_type, Path):
                continue  # a dangling navigation position — a no-op, dropped
            else:
                raise UncaughtDataError(
                    f"{node} produces {getattr(return_type, '__name__', return_type)} "
                    f"that is never stored or used — cache it with |= / &=, pass it to "
                    f"another node, or wrap it with cg.do()"
                )
        return steps

    ### Context manager ###

    @contextmanager
    def collect(self) -> Iterator[ProcedureContext]:
        """Make this the active context for the block, then [_finish][] it.

        ``_finish`` runs only on normal exit, *after* the active context is reset
        to the parent — so a wrapper node built there is emitted into the parent.
        """
        self._parent = ProcedureContext._active.get()
        token: Token = ProcedureContext._active.set(self)
        try:
            yield self
        finally:
            ProcedureContext._active.reset(token)
        self._finish()

    def _finish(self) -> None:
        """Reduce the collected nodes; subclass-specific.

        Runs with the parent active.
        """
        raise NotImplementedError

__init__() #

Start with no parent, nothing emitted, and nothing caught.

Source code in capturegraph-lib/capturegraph/procedures/procedure/authoring/contexts/base.py
def __init__(self) -> None:
    """Start with no parent, nothing emitted, and nothing caught."""
    self._parent: ProcedureContext | None = None
    self._emitted: list[Procedure[CGType]] = []
    self._caught: set[str] = set()

collect() #

Make this the active context for the block, then [_finish][] it.

_finish runs only on normal exit, after the active context is reset to the parent — so a wrapper node built there is emitted into the parent.

Source code in capturegraph-lib/capturegraph/procedures/procedure/authoring/contexts/base.py
@contextmanager
def collect(self) -> Iterator[ProcedureContext]:
    """Make this the active context for the block, then [_finish][] it.

    ``_finish`` runs only on normal exit, *after* the active context is reset
    to the parent — so a wrapper node built there is emitted into the parent.
    """
    self._parent = ProcedureContext._active.get()
    token: Token = ProcedureContext._active.set(self)
    try:
        yield self
    finally:
        ProcedureContext._active.reset(token)
    self._finish()

current() classmethod #

The active context, or raise if a node is built with none.

Source code in capturegraph-lib/capturegraph/procedures/procedure/authoring/contexts/base.py
@classmethod
def current(cls) -> ProcedureContext:
    """The active context, or raise if a node is built with none."""
    context = cls._active.get()
    if context is None:
        raise AuthoringError(
            "a procedure node was constructed with no active context — author "
            "it inside @cg.procedure, or wrap standalone construction in "
            "cg.detached()"
        )
    return context

current_or_none() classmethod #

The active context, or None if there is none.

Source code in capturegraph-lib/capturegraph/procedures/procedure/authoring/contexts/base.py
@classmethod
def current_or_none(cls) -> ProcedureContext | None:
    """The active context, or ``None`` if there is none."""
    return cls._active.get()