cli.invoke¶
Shared command-wrapper machinery: OttoContext injection + options expansion.
The plumbing behind both @instruction (otto run subcommands) and
@cli_command (top-level commands): a parameter annotated OttoContext
is stripped from the CLI signature and supplied at call time from the active
context, and an options dataclass parameter is expanded into individual CLI
flags. Factored out of cli/run.py so both decorators share one
implementation.
-
otto.cli.invoke.prepare_command_target(func: Callable[[...], Any], options_cls: type | None =
None) Callable[[...], Any]¶ Apply otto’s CLI wrappers to func: OttoContext injection + options expansion.
The shared machinery behind
@instructionand@cli_command: a parameter annotatedOttoContextis stripped from the CLI signature and injected at call time; an options_cls dataclass parameter is expanded into individual CLI flags.Idempotent by contract, not coincidence: a callable this function already wrapped is returned unchanged (sentinel attribute). The dispatch path prepares twice —
@cli_commandat decoration, thenresolve_spec_command’s function-loader branch, which serves every function loader and can’t know one was pre-prepared. Without the sentinel that was safe only because_inject_ctxhappens to strip the ctx annotation that triggers it.
-
exception otto.cli.invoke.LabContextError(message: str, exit_code: int, *, rich: bool =
True)¶ Bases:
ExceptionA lab-context failure carrying its user-facing message + exit code.
ensure_lab_context()raises this instead of printing directly, so a soft probe (try_ensure_lab(), used by the class-scopedotto hostmenu) can swallow it silently. The loud callers —command_preamble()and the root--show-lab/--list-hostsbranch — catch it and print (therichflag choosesrich.printvs a plain stderrtyper.echo) before re-raisingtyper.Exitwith the storedexit_code.
- otto.cli.invoke.report_lab_context_error(err: LabContextError) None¶
Print err’s message the loud way, then raise
typer.Exitwith its code.Shared by the loud lab-context callers so the exact user-facing text and exit codes live in one place. Rich messages go through
rich.print; non-rich messages (the plainMissing option '--lab'usage error) go to stderr viatyper.echoto match click’s own usage-error stream.
- class otto.cli.invoke.RootOptions(labs: list[str] | None, xdir: Path, log_days: int, log_level: str, rich_log_file: bool, show_time: bool, dry_run: bool, as_user: str | None, skip_reservation_check: bool)¶
Bases:
objectThe root-callback options the preamble needs, stashed on
ctx.meta.The root callback shrinks to recording these; the lab-loading / session-setup work reads them back lazily from
ctx.metaat the moment a real (non-help) command invocation begins.
- otto.cli.invoke.ensure_cli_session(ctx: Context) None¶
Print the banner and initialise CLI logging once per invocation (idempotent).
Split out of
ensure_lab_context()so a soft lab probe (class-scopedotto hostmenus viatry_ensure_lab()) never prints the banner or touches logging. Guarded byctx.meta['_otto_session_ready'].
- otto.cli.invoke.ensure_lab_context(ctx: Context) OttoContext¶
Load the lab, build reservation state, and install the runtime context (idempotent).
Enforces
--lab, builds the lab repository, loads the lab, synthesizes docker placeholder hosts, resolves reservation state (stashed onctx.meta['otto_reservation']), and installs anOttoContextviaset_context. Guarded byctx.meta['_otto_lab_ready']so repeated calls are cheap. No banner, no logging init, no output dir — those belong toensure_cli_session()/command_preamble().
- otto.cli.invoke.try_ensure_lab(ctx: Context) OttoContext | None¶
Soft variant of
ensure_lab_context(): return None instead of raising.Used by
HostGroupclass-scoping — a soft probe where any failure (no--lab, unknown lab, broken backend) simply means “no class scoping available”, falling back to the full unscoped verb menu.
- otto.cli.invoke.fail_loud_on_bootstrap_errors() None¶
Exit(1) when bootstrap contained any repo error — shared loud gate.
The per-error
warning:lines were already printed byentry()at startup; print ONLY the framed summary here (don’t re-print each error in red) — the summary points back at those warnings. Used by the leaf preamble AND the root--show-lab/--list-hostsbranch, so anything that inspects the registered world fails the same way.
- otto.cli.invoke.command_preamble(ctx: Context) None¶
Run once when a real (non-help) command invocation starts.
Order: bootstrap errors fail loud → lab-free commands are done → CLI session (banner/logging) → lab context → per-command output dir → reservation gate.
--helppaths never reach this function: click’s help option exits during leaf parse, beforeCommand.invoke.
- otto.cli.invoke.wrap_leaf_callbacks(cmd: Any, spec: CommandSpec) Any¶
Wrap every leaf command under cmd so its invoke runs the preamble first.
Wrapping
Command.invoke(not the callback) means the preamble runs only on real execution: a--helpon the leaf exits during parse and never reachesinvoke. Groups recurse into their static subcommands AND wrap theirget_commandso lazily-synthesized subcommands (e.g. the dynamicotto host <verb>commands) are wrapped on resolution too. Already-wrapped commands are skipped (resolution results are cached).
- otto.cli.invoke.make_registry_group(child_registry: Registry[Any]) type[TyperGroup]¶
Build a TyperGroup class whose children come from child_registry.
Children (suite/instruction sub-apps) convert lazily on first access. This follows the same idiom as
HostGroup(cli/expose.py): the group only resolves children on demand — it does NOT itself wrap them with the leaf-invoke preamble.main.py’s root dispatch wraps the WHOLE resolved group (and therefore every child it lazily resolves, viawrap_leaf_callbacks’sget_commandrecursion) with the preamble for the top-level spec ("run"/"test"). This keepsrun_app/suite_appusable standalone (e.g. in unit tests that invoke them directly viaCliRunnerwithout going through the fullottoroot app) whileotto run smoke/otto test TestXstill get the preamble when dispatched for real.