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']
module-attribute
¶
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())
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. |
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 |
epilog |
str
|
Optional long-form text rendered after the argparse
argument list in |
extras |
Mapping[str, Any]
|
Bag of package-specific metadata downstream consumers
ignore (shield's |
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
¶
is_group
property
¶
Whether this node carries children (i.e. is a verb group).
with_handler(handler)
¶
KeyRow
¶
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
roots
property
¶
The top-level verbs in this tree, in declaration order.
__iter__()
¶
__len__()
¶
__add__(other)
¶
Concatenate forests — other's roots appended to this one's.
Source code in src/terok_util/cli_types.py
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
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
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
walk()
¶
Yield (path, command) for every node in the tree, depth-first.
wire(target)
¶
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.
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
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.