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 |
target
instance-attribute
¶
__call__(*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
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. |
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 |
source |
str | None
|
When set, this node is a lazy reference — only
|
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).
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
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, *, 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
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.