Skip to content

navigation

navigation #

The fluent navigation and cache-assignment operators bound onto Procedure.

Navigating a procedure descends its return type one step: attribute access reads a struct field, indexing selects an array element (int) or a map entry (key), and .keys() / .values() enumerate a map. The same operators work whether the return type is a Path[…] (each step yields a deeper Path) or a plain value (each step yields the child value) — [.dispatch][] is the table that decides, so every operator here is just classify → guard → build.

Assignment caches: path |= value (skip if exists) and path &= value (overwrite) each record a CacheProcedure wrapped in a RequiredProcedure (so the stored value is a kept Void step) and return the CacheProcedure so the slot reads back as its value type.

These functions are imported into the Procedure class body, so every import here is lazy (the node and dispatch layers import Procedure back). The recorded sequence is owned by the active ProcedureContext, not by any per-node bookkeeping here.

__getitem__(self, key) #

__getitem__(
    self: Procedure[Array],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[CGType]
__getitem__(
    self: Procedure[Map],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[CGType]
__getitem__(
    self: Procedure[Path],
    key: int
    | Procedure[CGType]
    | JSONScalar
    | bool
    | float
    | str,
) -> Procedure[Path]

Index a navigable position or a value-access array / map.

On a navigated Path it selects an array element / map entry position (another Path). On a map value a node returns it selects the entry's value (MapKey); on the Array that a map's keys()/values() returns it selects the element value — a key scalar, or a Path[V] to navigate. The runtime dispatches all of these through the same navigable view.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __getitem__(
    self: Procedure[Path] | Procedure[Array] | Procedure[Map],
    key: int | Procedure[CGType] | JSONScalar | bool | float | str,
) -> Procedure[CGType]:
    """Index a navigable position or a value-access array / map.

    On a navigated ``Path`` it selects an array element / map entry position
    (another ``Path``). On a map *value* a node returns it selects the entry's
    value (``MapKey``); on the ``Array`` that a map's ``keys()``/``values()``
    returns it selects the element *value* — a key scalar, or a ``Path[V]`` to
    navigate. The runtime dispatches all of these through the same ``navigable`` view.
    """
    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is None or not (nav.is_array or nav.is_map):
        raise TypeError(f"{self} is not indexable (it is neither an array nor a map)")

    if nav.is_path:
        require_no_saved_value(cast("Procedure[Path]", self))
    child = subscript_child(cast("Procedure[Path]", self), nav, key, create=True)
    assert child is not None  # create=True always returns a child or raises
    return child

__iand__(self, value) #

path &= value — cache the value, overwriting any stored value.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __iand__[T: CGType](
    self: Procedure[Path[T]],
    value: Procedure[T],
) -> CacheProcedure[T]:
    """``path &= value`` — cache the value, overwriting any stored value."""
    return record_cache(self, value, skip_if_exists=False)

__ior__(self, value) #

path |= value — cache the value, skipping if already stored.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __ior__[T: CGType](
    self: Procedure[Path[T]],
    value: Procedure[T],
) -> CacheProcedure[T]:
    """``path |= value`` — cache the value, skipping if already stored."""
    return record_cache(self, value, skip_if_exists=True)

__setitem__(self, key, value) #

Absorb the arr[i] |= value / map[k] &= value rebind.

arr[i] |= value desugars to arr.__setitem__(i, arr[i].__ior__(value)); the cache was already recorded by __ior__, so this swallows it — but only when it is this exact slot's own cache (its destination is the navigation child for key), mirroring the strict struct-field __setattr__ guard. A plain arr[i] = value, or a cache built for a different slot, is rejected.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def __setitem__(
    self: Procedure[Path],
    key: int | Procedure[CGType] | JSONScalar | bool | float | str,
    value: CacheProcedure[CGType],
) -> None:
    """Absorb the ``arr[i] |= value`` / ``map[k] &= value`` rebind.

    ``arr[i] |= value`` desugars to ``arr.__setitem__(i, arr[i].__ior__(value))``;
    the cache was already recorded by ``__ior__``, so this swallows it — but only
    when it is *this exact slot's* own cache (its ``destination`` is the navigation
    child for ``key``), mirroring the strict struct-field ``__setattr__`` guard. A
    plain ``arr[i] = value``, or a cache built for a different slot, is rejected.
    """
    from capturegraph.procedures.nodes.destination.cache import (
        CacheProcedure,
    )
    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is not None and isinstance(value, CacheProcedure):
        if value.destination is subscript_child(self, nav, key, create=False):
            return
    raise TypeError(
        "cache at an indexed slot with '|=' (skip if exists) or '&=' (overwrite), "
        f"not plain assignment (tried to set [{key!r}])"
    )

exists(self) #

path.exists() — a Bool that is true when a value is stored here.

Reads the position from disk (LoadPath) and reports whether that read completes (ProcedureCompleted), so a missing value yields false instead of aborting the run. Pair it with cg.when(...) to run a block only once the position has been written. The read goes to disk regardless of any value cached at this slot this run, so it tests what was already present before the run.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def exists(self: Procedure[Path]) -> Procedure[Bool]:
    """``path.exists()`` — a ``Bool`` that is true when a value is stored here.

    Reads the position from disk (``LoadPath``) and reports whether that read
    completes (``ProcedureCompleted``), so a missing value yields ``false`` instead
    of aborting the run. Pair it with ``cg.when(...)`` to run a block only once the
    position has been written. The read goes to disk regardless of any value cached
    at this slot this run, so it tests what was already present before the run.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )
    from capturegraph.procedures.nodes.process.control import (
        ProcedureCompleted,
    )

    return ProcedureCompleted(procedure=LoadPath(source=self))

keys(self) #

A map position's keys as a read-only Array[K] value (index to select one).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def keys(self: Procedure[Path]) -> Procedure[Array]:
    """A map position's keys as a read-only ``Array[K]`` value (index to select one)."""
    from capturegraph.procedures.procedure.authoring.dispatch import (
        MAP_KEYS,
    )

    require_map_position(self, "keys")
    return cast(
        "Procedure[Array]",
        memoized_child(self, ("keys",), lambda: MAP_KEYS(source=self)),
    )

load(self) #

path.load() — read the value stored at this position.

Loading is explicit: an unwritten position used as a value never lowers to a disk read on its own. The read waits for a stored value (a prior run's, or one an interceptor fills); while nothing is stored it stays pending, so use [load_or][] when an absent value should produce a fallback instead. The read goes to disk regardless of any value cached at this slot this run, so it sees what was already present before the run.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def load[T: CGType](self: Procedure[Path[T]]) -> Procedure[T]:
    """``path.load()`` — read the value stored at this position.

    Loading is explicit: an unwritten position used as a value never lowers to a
    disk read on its own. The read waits for a stored value (a prior run's, or
    one an interceptor fills); while nothing is stored it stays pending, so use
    [load_or][] when an absent value should produce a fallback instead. The
    read goes to disk regardless of any value cached at this slot this run, so it
    sees what was already present before the run.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )

    # The compile-time validation pass checks the node against the schema
    # position, so the declared target is enforced before execution.
    return cast("Procedure[T]", LoadPath(source=self))

load_or(self, default) #

load_or(
    self: Procedure[Path[T]], default: Procedure[T]
) -> Procedure[T]
load_or(
    self: Procedure[Path], default: object
) -> Procedure[CGType]

path.load_or(default) — the stored value, or default's value.

Returned when nothing is stored here. The fallback runs only on a definitive "nothing stored" answer — never while the value is still loading — so a slow remote read cannot be mistaken for an absent one. default is a procedure (or raw literal) whose type matches the position's — so a typed default names the result's type, and the compile-time validation pass enforces the match.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def load_or(
    self: Procedure[Path],
    default: object,
) -> object:
    """``path.load_or(default)`` — the stored value, or ``default``'s value.

    Returned when nothing is stored here. The fallback runs only on a definitive
    "nothing stored" answer — never while the value is still loading — so a slow
    remote read cannot be mistaken for an absent one. ``default`` is a procedure (or
    raw literal) whose type matches the position's — so a typed default names the
    result's type, and the compile-time validation pass enforces the match.
    """
    from capturegraph.procedures.nodes.destination.access import (
        LoadPath,
    )
    from capturegraph.procedures.procedure.authoring.bridge import (
        coerce_input,
    )

    return LoadPath(source=self, default=coerce_input(default))

memoized_child(parent, key, factory) #

The parent's navigation child for key, built once.

Dedup is node-local: each node remembers its own children, so repeated navigation to the same slot returns the same node — which is what carries _assigned_value — with no central bookkeeping. A non-empty _children also marks a path slot a container, so saving a value there is rejected.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def memoized_child(
    parent: Procedure[CGType],
    key: tuple[object, ...],
    factory: Callable[[], Procedure[CGType]],
) -> Procedure[CGType]:
    """The ``parent``'s navigation child for ``key``, built once.

    Dedup is node-local: each node remembers its own children, so repeated
    navigation to the same slot returns the same node — which is what carries
    ``_assigned_value`` — with no central bookkeeping. A non-empty ``_children``
    also marks a *path* slot a container, so saving a value there is rejected.
    """
    existing = parent._children.get(key)
    if existing is not None:
        return existing
    child = factory()
    parent._children[key] = child
    return child

navigate(self, name) #

Read a struct field; bound as Procedure.__getattr__.

E.g. root.reference, CapturePanorama().preview. Only missing attributes reach here, and dunders never navigate.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def navigate(self: Procedure[Path], name: str) -> Procedure[Path]:
    """Read a struct field; bound as ``Procedure.__getattr__``.

    E.g. ``root.reference``, ``CapturePanorama().preview``. Only missing attributes
    reach here, and dunders never navigate.
    """
    if name.startswith("__") and name.endswith("__"):
        raise AttributeError(name)

    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(self)
    if nav is None or not nav.has_field(name):
        raise AttributeError(name)

    if nav.is_path:
        require_no_saved_value(self)
    child = memoized_child(self, ("field", name), lambda: nav.nodes.field(source=self, field=name))
    return cast("Procedure[Path]", child)

record_cache(position, value, *, skip_if_exists) #

Record a cache at position and return it, typed as its value T.

Builds the CacheProcedure, wraps it in a RequiredProcedure (so the stored value is a Void step the context keeps, with the cache as its caught child), and records the assignment on the slot for read-back. A slot that has been navigated into (non-empty _children) is a container, and one already assigned cannot be assigned again.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def record_cache[T: CGType](
    position: Procedure[Path[T]],
    value: Procedure[T],
    *,
    skip_if_exists: bool,
) -> CacheProcedure[T]:
    """Record a cache at ``position`` and return it, typed as its value ``T``.

    Builds the ``CacheProcedure``, wraps it in a ``RequiredProcedure`` (so the
    stored value is a ``Void`` step the context keeps, with the cache as its
    caught child), and records the assignment on the slot for read-back. A slot
    that has been navigated into (non-empty ``_children``) is a container, and one
    already assigned cannot be assigned again.
    """
    from capturegraph.procedures.nodes.destination.cache import (
        CacheProcedure,
    )
    from capturegraph.procedures.nodes.process.structural import (
        RequiredProcedure,
    )
    from capturegraph.procedures.procedure.authoring.bridge import (
        coerce_input,
    )
    from capturegraph.procedures.procedure.authoring.errors import (
        AlreadyAssignedError,
        AuthoringError,
    )
    from capturegraph.procedures.procedure.authoring.paths import (
        render_path,
    )

    if position._children:
        raise AuthoringError(
            f"cannot save to navigated directory {render_path(position)} "
            "(a slot is a value or a container, not both)"
        )
    if position._assigned_value is not None:
        raise AlreadyAssignedError(f"{render_path(position)} is assigned more than once")

    cache = CacheProcedure(
        destination=position,
        value=coerce_input(value),
        skip_if_exists=skip_if_exists,
    )
    RequiredProcedure(procedure=cache)  # = cg.do(cache): the kept Void step; catches the cache
    object.__setattr__(position, "_assigned_value", cache)
    return cache

require_map_position(node, op) #

Guard .keys() / .values(): both enumerate a Path[Map] position.

A captured map value supports [key] indexing only.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def require_map_position(node: Procedure[Path], op: str) -> None:
    """Guard ``.keys()`` / ``.values()``: both enumerate a ``Path[Map]`` position.

    A captured map *value* supports ``[key]`` indexing only.
    """
    from capturegraph.procedures.procedure.authoring.dispatch import (
        navigable,
    )

    nav = navigable(node)
    if nav is None or not nav.is_map:
        raise TypeError(f"{node} has no {op} (it is not a map)")
    if not nav.is_path:
        raise TypeError(
            f"{node} returns a map value; .{op}() enumerates a Path[Map] position — "
            "index a captured map by key instead (map[key])"
        )
    require_no_saved_value(node)

require_no_saved_value(node) #

Guard descending into a path position.

A slot is a value or a container, not both, so a slot with a saved value has no children.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def require_no_saved_value(node: Procedure[Path]) -> None:
    """Guard descending into a *path* position.

    A slot is a value or a container, not both, so a slot with a saved value has no
    children.
    """
    if node._assigned_value is not None:
        from capturegraph.procedures.procedure.authoring.errors import (
            AuthoringError,
        )
        from capturegraph.procedures.procedure.authoring.paths import (
            render_path,
        )

        raise AuthoringError(
            f"cannot navigate into {render_path(node)}: a value was saved there "
            "(a slot is a value or a container, not both)"
        )

string_literal(key) #

A constant string key as a plain str, or None if it can't be one.

Accepts a raw str or a String value; returns None when the key is computed and so can't be checked statically.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def string_literal(key: object) -> str | None:
    """A constant string key as a plain ``str``, or ``None`` if it can't be one.

    Accepts a raw ``str`` or a ``String`` value; returns ``None`` when the key is
    computed and so can't be checked statically.
    """
    from capturegraph.types.scalars.primitives.string import (
        String,
    )

    if isinstance(key, str):
        return key
    if isinstance(key, String):
        return key.value
    return None

subscript_child(node, nav, key, *, create) #

Resolve node[key] — an array element (int) or a map entry (key value).

With create set this builds and memoizes the child; otherwise it only looks up an already-built one (so __setitem__ can match a slot's own cache without re-navigating an already-saved position).

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def subscript_child(
    node: Procedure[Path],
    nav: Navigable,
    key: int | Procedure[CGType] | JSONScalar | bool | float | str,
    *,
    create: bool,
) -> Procedure[CGType] | None:
    """Resolve ``node[key]`` — an array element (int) or a map entry (key value).

    With ``create`` set this builds and memoizes the child; otherwise it only looks
    up an already-built one (so ``__setitem__`` can match a slot's own cache
    without re-navigating an already-saved position).
    """
    if nav.is_array:
        if not isinstance(key, int):
            raise TypeError(f"array index must be an int literal, got {key!r}")
        memo = ("index", key)
        if not create:
            return node._children.get(memo)
        return memoized_child(node, memo, lambda: nav.nodes.index(source=node, index=key))

    from capturegraph.procedures.procedure.authoring.bridge import (
        coerce_input,
    )

    key_node = coerce_input(key)
    memo = ("key", key_node.uuid)
    if not create:
        return node._children.get(memo)
    if not nav.is_path:
        warn_unknown_map_key(node, key)
    return memoized_child(node, memo, lambda: nav.nodes.key(source=node, key=key_node))

values(self) #

A map position's entry positions as Array[Path[V]].

Fan out, or index to select one to navigate.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def values(self: Procedure[Path]) -> Procedure[Array]:
    """A map position's entry positions as ``Array[Path[V]]``.

    Fan out, or index to select one to navigate.
    """
    from capturegraph.procedures.procedure.authoring.dispatch import (
        MAP_VALUES,
    )

    require_map_position(self, "values")
    return cast(
        "Procedure[Array]",
        memoized_child(self, ("values",), lambda: MAP_VALUES(source=self)),
    )

warn_unknown_map_key(source, key) #

Warn when a value-level map[key] names a key the source map provably lacks.

Only a node that advertises its keys (static_map_keys) with a literal key is checked; a dynamic source or computed key is left alone. The access still builds — at runtime it yields no value, and the client reports the unknown key.

Source code in capturegraph-lib/capturegraph/procedures/procedure/methods/navigation.py
def warn_unknown_map_key(source: Procedure[CGType], key: object) -> None:
    """Warn when a value-level ``map[key]`` names a key the source map provably lacks.

    Only a node that advertises its keys (``static_map_keys``) with a literal key is
    checked; a dynamic source or computed key is left alone. The access still
    builds — at runtime it yields no value, and the client reports the unknown key.
    """
    import warnings

    known = source.static_map_keys()
    literal = string_literal(key)
    if known is None or literal is None or literal in known:
        return
    warnings.warn(
        f"{source} has no key {literal!r}; its keys are {sorted(known)}. "
        f"This access produces no value at runtime.",
        stacklevel=4,  # warn ← here ← subscript_child ← __getitem__ ← user code
    )