Skip to content

cli_types

cli_types

Shared vocabulary for the command registry — argument and command definitions.

Every per-subsystem command module in a sibling terok package imports from here. Out-of-tree consumers (terok, terok-executor, terok-shield, terok-clearance) build their CLI frontends against these types without depending on any handler implementation.

A CommandDef is either a leaf (handler set, children empty) or a group (handler is None, children holds subverbs). Groups can nest arbitrarily — vault passphrase seal is a leaf inside the passphrase group inside the vault group. A package's whole CLI is a forest of these, wrapped in a CommandTree for composition + argparse wiring.

__all__ = ['ArgDef', 'CommandDef', 'CommandTree', 'KeyRow', 'LazyHandler'] module-attribute

LazyHandler(target) dataclass

A command handler that imports its target on first call.

Registries wire handler=LazyHandler("pkg.module:function") instead of importing the function eagerly, so building a CommandTree pulls in none of the handler modules — only dispatch of an actually-selected verb imports the one module it needs. For a CLI with a dozen subsystems (vault, gate, shield, …) that turns a full-forest import into a single-leaf one, which is the whole cost of starting the process for a single command.

The wrapper is an opaque Callable: wire, overlay, and dispatch treat it exactly like a plain function, and dispatch detects coroutines from the return value, so async handlers work unchanged. Resolution is process-cached, so repeated dispatch pays the import once.

Attributes:

Name Type Description
target str

A "module.path:qualname" string. qualname may be dotted to reach a nested attribute (e.g. a classmethod).

target instance-attribute

__call__(*args, **kwargs)

Import the target on first use and invoke it.

Source code in src/terok_util/cli_types.py
def __call__(self, *args: Any, **kwargs: Any) -> Any:
    """Import the target on first use and invoke it."""
    return _resolve_handler(self.target)(*args, **kwargs)

resolve()

Import and return the underlying callable (cached).

The explicit escape hatch for code that must introspect the real handler — e.g. inspect its signature to decide whether to wrap it. Calling this imports the target module, so a caller that wants to stay lazy should reach for it only on a path that will load the handler anyway.

Source code in src/terok_util/cli_types.py
def resolve(self) -> Callable[..., Any]:
    """Import and return the underlying callable (cached).

    The explicit escape hatch for code that must introspect the real
    handler — e.g. inspect its signature to decide whether to wrap it.
    Calling this imports the target module, so a caller that wants to
    stay lazy should reach for it only on a path that will load the
    handler anyway.
    """
    return _resolve_handler(self.target)

ArgDef(name, help='', type=None, default=None, action=None, dest=None, nargs=None, required=False) dataclass

Definition of a single CLI argument.

name instance-attribute

help = '' class-attribute instance-attribute

type = None class-attribute instance-attribute

default = None class-attribute instance-attribute

action = None class-attribute instance-attribute

dest = None class-attribute instance-attribute

nargs = None class-attribute instance-attribute

required = False class-attribute instance-attribute

CommandDef(name, help='', handler=None, args=(), children=(), group='', epilog='', extras=dict(), source=None) dataclass

One node in a command tree — a leaf verb or a group of verbs.

Attributes:

Name Type Description
name str

Verb name as it appears on the CLI.

help str

One-line help string.

handler Callable[..., Any] | None

Callable implementing the verb. None for groups.

args tuple[ArgDef, ...]

Argument definitions parsed by argparse.

children tuple[CommandDef, ...]

Sub-verbs. Non-empty makes this node a group.

group str

Free-form tag used by per-subsystem grouping (unrelated to the children structural nesting).

epilog str

Optional long-form text rendered after the argparse argument list in --help output.

extras Mapping[str, Any]

Bag of package-specific metadata downstream consumers ignore (shield's needs_container / standalone_only would live here on a unified shape).

source str | None

When set, this node is a lazy reference — only name and help are populated (enough to render the top-level --help listing), and source is a "module:qualname" dotted path resolving to the fully populated CommandDef for this verb. CommandTree.wire resolves it (importing the module) only when the verb is the one actually invoked, so a multi-subsystem CLI imports one verb's module per run instead of all of them.

A frozen-dataclass + structural sharing is the load-bearing part of the wrap-once-share-everywhere story: when a consumer overlays a handler at one path, the modified CommandDef is referenced from every shortcut that also points at that path. Identity is what makes the overlay propagate.

name instance-attribute

help = '' class-attribute instance-attribute

handler = None class-attribute instance-attribute

args = () class-attribute instance-attribute

children = () class-attribute instance-attribute

group = '' class-attribute instance-attribute

epilog = '' class-attribute instance-attribute

extras = field(default_factory=dict) class-attribute instance-attribute

source = None class-attribute instance-attribute

is_group property

Whether this node carries children (i.e. is a verb group).

is_lazy property

Whether this node defers to a source module.

resolve()

Return the fully-populated node — importing source on first use.

A non-lazy node is its own resolution. A lazy one imports its "module:qualname" target (cached) and returns the real CommandDef it names, so the verb's module — and only that verb's module — is loaded.

Source code in src/terok_util/cli_types.py
def resolve(self) -> CommandDef:
    """Return the fully-populated node — importing ``source`` on first use.

    A non-lazy node is its own resolution.  A lazy one imports its
    ``"module:qualname"`` target (cached) and returns the real
    [`CommandDef`][terok_util.cli_types.CommandDef] it names, so the
    verb's module — and only that verb's module — is loaded.
    """
    if self.source is None:
        return self
    return cast("CommandDef", _resolve_handler(self.source))

with_handler(handler)

Return a copy with handler replaced — pure leaf-rewrap.

Source code in src/terok_util/cli_types.py
def with_handler(self, handler: Callable[..., Any]) -> CommandDef:
    """Return a copy with ``handler`` replaced — pure leaf-rewrap."""
    return replace(self, handler=handler)

with_children(children)

Return a copy with children replaced.

Source code in src/terok_util/cli_types.py
def with_children(self, children: tuple[CommandDef, ...]) -> CommandDef:
    """Return a copy with ``children`` replaced."""
    return replace(self, children=children)

KeyRow

Bases: NamedTuple

One registered SSH key, fully resolved for display and matching.

scope instance-attribute

comment instance-attribute

key_type instance-attribute

fingerprint instance-attribute

private_key instance-attribute

public_key instance-attribute

CommandTree(roots)

A forest of CommandDef nodes.

The unit of composition for CLI registries: each package exposes its own CommandTree; consumers walk it structurally, overlay handlers where they wrap a concept, extend with their own verbs, and wire the result into argparse.

Composition is identity-preserving — nodes the consumer doesn't touch share object identity with their pre-overlay counterparts, so a shortcut that splices the same subtree at the consumer's top level reaches the same modified handler. terok shield install and terok executor sandbox shield install resolving to the same wrap is a direct consequence.

Build a tree from an iterable of top-level verbs/groups.

Source code in src/terok_util/cli_types.py
def __init__(self, roots: Iterable[CommandDef]) -> None:
    """Build a tree from an iterable of top-level verbs/groups."""
    self._roots: tuple[CommandDef, ...] = tuple(roots)

roots property

The top-level verbs in this tree, in declaration order.

__iter__()

Yield each root verb.

Source code in src/terok_util/cli_types.py
def __iter__(self) -> Iterator[CommandDef]:
    """Yield each root verb."""
    return iter(self._roots)

__len__()

Number of root verbs.

Source code in src/terok_util/cli_types.py
def __len__(self) -> int:
    """Number of root verbs."""
    return len(self._roots)

__add__(other)

Concatenate forests — other's roots appended to this one's.

Source code in src/terok_util/cli_types.py
def __add__(self, other: CommandTree | Iterable[CommandDef]) -> CommandTree:
    """Concatenate forests — *other*'s roots appended to this one's."""
    other_roots = other.roots if isinstance(other, CommandTree) else tuple(other)
    return CommandTree(self._roots + other_roots)

find_at(path)

Return the CommandDef at path.

path is a sequence of verb names from the root. An empty path is rejected (no synthetic root). KeyError if any segment doesn't match a child name.

Source code in src/terok_util/cli_types.py
def find_at(self, path: Sequence[str]) -> CommandDef:
    """Return the [`CommandDef`][terok_util.cli_types.CommandDef] at *path*.

    *path* is a sequence of verb names from the root.  An empty
    path is rejected (no synthetic root).  ``KeyError`` if any
    segment doesn't match a child name.
    """
    if not path:
        raise KeyError("empty path; specify at least one verb name")
    first, *rest = path
    for root in self._roots:
        if root.name == first:
            return _descend(root, tuple(rest))
    raise KeyError(f"no top-level verb {first!r}")

overlay(overrides)

Return a new tree with handlers replaced at the named paths.

overrides maps verb-name tuples (e.g. ("vault", "status")) to replacement handlers. Each match produces one new CommandDef via replace; ancestors are likewise replaced because their children tuples now hold a new node, but unrelated siblings share identity with the input tree.

Sandbox-vocab paths use the operator-facing verb names — same names you'd type on the CLI — so the override map reads like a routing table.

Source code in src/terok_util/cli_types.py
def overlay(self, overrides: Mapping[tuple[str, ...], Callable[..., Any]]) -> CommandTree:
    """Return a new tree with handlers replaced at the named paths.

    *overrides* maps verb-name tuples (e.g. ``("vault", "status")``)
    to replacement handlers.  Each match produces one new
    [`CommandDef`][terok_util.cli_types.CommandDef] via ``replace``;
    ancestors are likewise replaced because their ``children``
    tuples now hold a new node, but unrelated siblings share
    identity with the input tree.

    Sandbox-vocab paths use the operator-facing verb names — same
    names you'd type on the CLI — so the override map reads like a
    routing table.
    """
    return CommandTree(_overlay_forest(self._roots, dict(overrides), ()))

extend_at(path, additions)

Return a new tree with additions appended at the path's children.

Empty path extends the top-level forest. Otherwise the CommandDef at path must be a group — a leaf (one with handler set and no children) cannot be extended; trying to do so would produce a hybrid node argparse can't represent (handler + subparsers on the same parser). Raises TypeError rather than silently inventing such a node.

Source code in src/terok_util/cli_types.py
def extend_at(self, path: Sequence[str], additions: Iterable[CommandDef]) -> CommandTree:
    """Return a new tree with *additions* appended at the path's children.

    Empty path extends the top-level forest.  Otherwise the
    [`CommandDef`][terok_util.cli_types.CommandDef] at *path*
    must be a **group** — a leaf (one with ``handler`` set and no
    ``children``) cannot be extended; trying to do so would produce
    a hybrid node argparse can't represent (handler + subparsers
    on the same parser).  Raises ``TypeError`` rather than
    silently inventing such a node.
    """
    addition_tuple = tuple(additions)
    if not path:
        return CommandTree(self._roots + addition_tuple)
    target = self.find_at(path)
    if target.handler is not None and not target.children:
        raise TypeError(
            f"cannot extend leaf {'.'.join(path)!r}: extend_at requires a group "
            f"(handler=None with children), but the target has a handler set"
        )
    return CommandTree(_extend_forest(self._roots, tuple(path), addition_tuple, ()))

walk()

Yield (path, command) for every node in the tree, depth-first.

Source code in src/terok_util/cli_types.py
def walk(self) -> Iterator[tuple[tuple[str, ...], CommandDef]]:
    """Yield ``(path, command)`` for every node in the tree, depth-first."""
    for root in self._roots:
        yield from _walk_node(root, ())

wire(target, *, argv=None)

Wire this tree's verbs as subparsers under target, recursively.

target may be either an ArgumentParser (a fresh add_subparsers() action is created) or an existing argparse._SubParsersAction (the tree mounts straight under it; the private name has no public docs target). The second form lets a consumer mix legacy register-style subparsers with structural CommandTree ones under the same root parser without colliding on argparse's one-subparsers-per-parser rule.

Lazy dispatch: pass argv (the process args) to load only the invoked verb's module. A lazy root (one with source set) that isn't the invoked verb is wired as a name-and-help placeholder — enough for the top-level --help listing — while the one verb the user actually typed is resolved and wired in full. The whole tree is still materialised for shell completion (_ARGCOMPLETE in the environment), for a bare/--help invocation with no verb, and for an unrecognised leading token (so argparse can error against the full choice list). Omit argv (the default) to wire everything eagerly, unchanged.

The same CommandDef wired at multiple positions (deep nesting + shortcuts) yields independent argparse subparser instances, but each subparser's dispatch reads back the same handler object — so concept translations applied via overlay apply uniformly across every entry point that references the modified node.

Source code in src/terok_util/cli_types.py
def wire(
    self,
    target: argparse.ArgumentParser | argparse._SubParsersAction,
    *,
    argv: list[str] | None = None,
) -> None:
    """Wire this tree's verbs as subparsers under *target*, recursively.

    *target* may be either an
    [`ArgumentParser`][argparse.ArgumentParser] (a fresh
    ``add_subparsers()`` action is created) or an existing
    ``argparse._SubParsersAction`` (the tree mounts straight under
    it; the private name has no public docs target).  The second
    form lets a consumer mix legacy register-style subparsers
    with structural
    [`CommandTree`][terok_util.cli_types.CommandTree] ones under
    the same root parser without colliding on argparse's
    one-subparsers-per-parser rule.

    **Lazy dispatch:** pass *argv* (the process args) to load only the
    invoked verb's module.  A [lazy root][terok_util.cli_types.CommandDef]
    (one with ``source`` set) that isn't the invoked verb is wired as a
    name-and-help *placeholder* — enough for the top-level ``--help``
    listing — while the one verb the user actually typed is
    [resolved][terok_util.cli_types.CommandDef.resolve] and wired in
    full.  The whole tree is still materialised for shell completion
    (``_ARGCOMPLETE`` in the environment), for a bare/``--help``
    invocation with no verb, and for an unrecognised leading token (so
    argparse can error against the full choice list).  Omit *argv*
    (the default) to wire everything eagerly, unchanged.

    The same [`CommandDef`][terok_util.cli_types.CommandDef]
    wired at multiple positions (deep nesting + shortcuts) yields
    independent argparse subparser instances, but each subparser's
    dispatch reads back the same handler object — so concept
    translations applied via ``overlay`` apply uniformly across
    every entry point that references the modified node.
    """
    if isinstance(target, argparse._SubParsersAction):
        sub = target
    else:
        sub = target.add_subparsers()
    # ``source`` is read via getattr so slimmer foreign CommandDef shapes
    # (shield / clearance define their own dataclasses) wire through the
    # same path — they simply never look lazy.
    lazy_names = {cmd.name for cmd in self._roots if getattr(cmd, "source", None)}
    verb = _first_verb(argv) if argv is not None else None
    wire_everything = (
        argv is None
        or "_ARGCOMPLETE" in os.environ
        or (verb is not None and verb not in lazy_names)
    )
    for cmd in self._roots:
        if getattr(cmd, "source", None) and not wire_everything and cmd.name != verb:
            sub.add_parser(cmd.name, help=cmd.help)  # placeholder for --help listing
        else:
            _wire_command(sub, cmd.resolve() if getattr(cmd, "source", None) else cmd)

dispatch(args) staticmethod

Invoke the handler stored on args by CommandTree.wire.

Bridges argparse's parsed-args namespace to the handler kwargs the CommandDef declared. Async handlers are detected and run via asyncio.run so consumers don't need separate dispatch paths per handler flavour. A handler that returns an int exits the process with that status (None — the usual return — is a clean exit 0), so exit codes a handler computes reach the shell.

Source code in src/terok_util/cli_types.py
@staticmethod
def dispatch(args: argparse.Namespace) -> None:
    """Invoke the handler stored on *args* by [`CommandTree.wire`][terok_util.cli_types.CommandTree.wire].

    Bridges argparse's parsed-args namespace to the handler kwargs
    the [`CommandDef`][terok_util.cli_types.CommandDef] declared.
    Async handlers are detected and run via ``asyncio.run`` so
    consumers don't need separate dispatch paths per handler
    flavour.  A handler that returns an ``int`` exits the process
    with that status (``None`` — the usual return — is a clean
    exit 0), so exit codes a handler computes reach the shell.
    """
    cmd: CommandDef | None = getattr(args, "_cmd", None)
    if cmd is None:
        # Argparse stopped before a leaf command (bare ``terok`` /
        # ``terok shield`` etc.).  Argparse already printed help; exit
        # with the same "no command given" status it would.
        raise SystemExit(2)
    if cmd.handler is None:
        raise SystemExit(f"Command {cmd.name!r} has no handler")
    kwargs = {_arg_dest(arg): getattr(args, _arg_dest(arg), arg.default) for arg in cmd.args}
    # Trailing args after ``--`` (currently only ``run`` consumes them).
    if hasattr(args, "podman_args"):
        kwargs["podman_args"] = args.podman_args
    result = cmd.handler(**kwargs)
    if inspect.iscoroutine(result):
        import asyncio  # noqa: PLC0415

        result = asyncio.run(result)
    # A handler that returns an ``int`` means it as a process exit
    # status — propagate it so the code survives to the shell (a
    # supervisor's wrapper-retry code, a child's failure code).
    # ``None`` (the common case) and every other return type stay a
    # clean exit 0.  ``bool`` is excluded: it subclasses ``int`` but a
    # handler returning ``True``/``False`` means success, not code 1/0.
    if isinstance(result, int) and not isinstance(result, bool):
        raise SystemExit(result)