CLI API¶
Python API for building CLI programs from Pydantic models.
from dracon import Arg, Subcommand, subcommand, dracon_program, ConfigFile, HelpSection, make_program, make_callable
Arg¶
Factory function that builds a CliParam(source='model', ...) record — the unified record type for CLI parameters. (CliParam also backs YAML-declared flags via !require / !set_default mapping bodies, where it's built with source='yaml'.) Arg is the ergonomic way to attach CLI metadata to a Pydantic field via Annotated:
class Config(BaseModel):
name: Annotated[str, Arg(short="n", help="Your name")]
output: Annotated[str, Arg(positional=True, is_file=True)]
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
help |
str |
None |
Help text. Falls back to field's description. |
short |
str |
None |
Short flag (e.g. "n" for -n). |
long |
str |
None |
Long flag (e.g. "name" for --name). Auto-generated from field name if not set. |
positional |
bool |
False |
Treat as a positional argument instead of --flag. |
is_file |
bool |
False |
Treat the value as a config file path: the file is loaded and composed as YAML, then merged into the field. Supports selectors (model.yaml@encoder). |
is_flag |
bool |
None |
Force flag behavior (no value). None = auto-detect from bool type. |
action |
Callable |
None |
Callback (program, value) -> Any triggered when the flag is parsed. |
default_str |
str |
None |
Override the default value display in help text. |
auto_dash_alias |
bool |
None |
Replace _ with - in the long flag. None inherits from the program default (True). |
raw |
bool |
False |
Skip YAML composition; pass the CLI string value as-is. |
subcommand |
bool |
False |
Mark this field as a subcommand discriminator (usually set via Subcommand() instead). |
Subcommand¶
Type factory for discriminated union subcommands.
Signature¶
Returns Annotated[Union[cmd_types...], Field(discriminator=...), Arg(subcommand=True, positional=True)].
The **arg_kwargs are forwarded to the inner Arg.
@subcommand¶
Decorator that injects a Literal discriminator field into a BaseModel.
@subcommand("train")
class TrainCmd(BaseModel):
epochs: int = 10
# 'action' field is auto-injected: action: Literal["train"] = "train"
Signature¶
@dracon_program¶
Decorator that turns a Pydantic BaseModel into a full CLI program.
@dracon_program(name="mytool", version="1.0")
class Config(BaseModel):
input: Annotated[str, Arg(positional=True)]
verbose: bool = False
Config.cli() # parse sys.argv
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
Class name | Program name in help output. |
version |
str |
None |
Version string (shown with --version). |
description |
str |
Class docstring | Help description text. |
context_types |
list[type] |
None |
Types added to interpolation context as {TypeName: type}. |
context |
dict |
None |
Additional interpolation context. |
deferred_paths |
list[str] |
[] |
Paths forced to DeferredNode. |
auto_context |
bool |
False |
Capture types from the decorator's call site namespace. |
sections |
list[HelpSection] |
None |
Extra sections appended to help output. |
epilog |
str |
None |
Text at the bottom of help output. |
config_files |
list[ConfigFile] |
[] |
Auto-discovered config files, loaded as the base layer (below CLI args). |
Generated Methods¶
All methods are classmethods on the decorated class:
.cli(argv=None)¶
Parse CLI args (defaults to sys.argv[1:]), construct the model, and call .run() if defined. Config files from config_files are auto-discovered and loaded as the base layer.
.invoke(*configs, **context_kwargs)¶
Load from config file paths and context, construct, then call .run().
.from_config(*configs, **context_kwargs)¶
Like .invoke() but returns the model instance without calling .run().
.load(path, context=None)¶
Low-level: load a single config file and validate as the model type.
Built-in Flags¶
Every @dracon_program includes:
| Flag | Description |
|---|---|
-h, --help |
Print help panel and exit. |
--trace PATH |
Show composition provenance for a dotted keypath. |
--trace-all |
Show provenance for all values. |
CLI Argument Parsing¶
The mental model is one rule: --name value targets anything declared, anywhere —
a Pydantic field on the program model, or a top-level !require / !set_default
in any layered config. Both surfaces share the same argparse flag set and the
same --help panel.
+file.yaml-- load as an additional config layer (merged left to right)--flag valueor--flag=value-- set a named option (model field or YAML-declared variable)--nested.path value-- set a nested model field by dotted path++var=valueor++var value-- set a context variable directly (escape hatch, see below)--define.var=value-- long form of++- Short flags cannot be combined: use
-c -r -j, not-crj
++ and --define. stay as the generic context-injection rail. Reach for
them when a flag has no declaration, when a model field shadows a YAML
variable of the same name, or when you want to feed a raw YAML literal that
sidesteps argparse coercion.
Spelling: dashes with --, underscores with ++. The --flag form
auto-dash-aliases underscore names — a !set_default training_set_file: (or a
model field training_set_file) is reached as --training-set-file, and the
underscore spelling --training_set_file is rejected as unknown. ++ and
--define. do the opposite: they take the variable's literal name with no
aliasing, so it's ++training_set_file=…. An accidental ++training-set-file=…
does not error — it just sets an unrelated variable that nothing reads (you'll
see it in the unused-variable warning).
CLI flags from config layers¶
The flag mechanism described above is unified: it covers model fields and
top-level !require / !set_default directives in any +-layered config.
Adding a new flag for an experiment is a config edit, not a code edit — the
same --name value rail handles it.
# plugins/analytics.yaml
!require api_key:
help: "API key for the analytics service"
!set_default:int batch_size:
default: 32
help: "batch size"
short: -b
mycli +plugins/analytics.yaml --api-key $SECRET -b 64
mycli +plugins/analytics.yaml --help # shows --api-key and --batch-size
What makes a directive CLI-visible¶
- It is at the top level of a layered config (a
+file.yamlargument). - It is
!requireor!set_default(typed variants like!set_default:intcount too, withintbecoming the argparsetype=). - It is not nested inside
!fn,!deferred, or!ifbranches — those are inner scopes by construction.
Precedence¶
When the same name is declared in multiple places, the resolution order is:
Specifically:
- A model field shadows a YAML directive of the same name.
--portroutes to the model field; the YAML variable is still reachable via++port=...or--define.port=.... - Multiple layers declaring the same name: last layer wins for the argparse metadata (help/short/default).
- A short alias that collides with an existing model-side
Argis dropped with one warning; the long flag still works.
Why ++ still exists¶
++name=value and --define.name=value bypass all flag discovery. They
stay because they cover three cases the unified flag rail cannot:
- A model field shadows a YAML variable of the same name;
++writes the YAML variable explicitly. - A name has no declaration anywhere — pure ad-hoc context injection.
- A discovered flag's argparse coercion gets in the way;
++accepts a raw YAML literal instead (e.g.++weights="[0.1, 0.2]").
--help rendering¶
When more than two YAML-declared flags are discovered, --help splits them
into per-source-file subsections under the model-side Options: panel:
Usage: myapp [OPTIONS]
Options:
-e, --env <env> environment name
--workers <workers> worker count [default: 4]
-h, --help Print this help message
Options from analytics.yaml:
--api-key <api_key> API key for the analytics service
-b, --batch-size <int> events per upload batch
Options from db.yaml:
--db-host <db_host> database host [default: localhost]
--db-port <int> database port [default: 5432]
With two or fewer YAML flags total, the help stays flat (one Options:
panel) so small layered surfaces don't get visually noisy. Layered files
whose source path can't be resolved (rare — only when the static-scan
fallback fires) bucket under "Options from layered configs:".
Inspecting at runtime¶
Set DRACON_SHOW_VARS=1 to print a table of every defined variable at the
end of a CLI run. The Source column distinguishes CLI (++/--define)
from CLI (--flag) (a discovered flag) and config (!define) (a value
that came from the composed YAML itself).
The unused-variable warning fires when a variable was set on the CLI
(via any of the above) but no ${...} interpolation, no !set_default,
and no !require ever read it.
ConfigFile¶
Declares a config file for auto-discovery.
@dracon_program(config_files=[
ConfigFile("~/.mytool/config.yaml"),
ConfigFile(".mytool.yaml", search_parents=True),
])
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str |
required | File path. Supports ~ expansion. |
search_parents |
bool |
False |
Walk up the directory tree looking for the file. Uses cascade: loader internally. Must be a relative path. |
required |
bool |
False |
Raise FileNotFoundError if not found. |
selector |
str |
None |
Keypath selector appended as @selector to the include string. |
HelpSection¶
Extra section in CLI help output.
make_program¶
Low-level factory. Creates a Program object without decorating a class.
prog = make_program(Config, name="mytool", version="1.0")
instance, raw_args = prog.parse_args(["--input", "data.csv"])
make_callable¶
Turn a YAML config file or DeferredNode into a reusable callable.
from dracon import make_callable
fn = make_callable("file:template.yaml", context_types=[MyModel])
result = fn(param1="value", param2=42)
Signature¶
make_callable(
path_or_node: str | Path | DeferredNode,
context: dict = None,
context_types: list[type] = None,
auto_context: bool = False,
**loader_kwargs,
)
When given a file path, the entire file is loaded with deferred_paths=['/'] to produce a DeferredNode. The returned callable accepts **kwargs that are injected as context, then constructs the result.
| Parameter | Description |
|---|---|
path_or_node |
File path string or existing DeferredNode. |
context |
Base context dict (types, functions, values). |
context_types |
List of types added as {TypeName: type}. |
auto_context |
Capture types from the caller's namespace. |
**loader_kwargs |
Forwarded to DraconLoader (e.g. deferred_paths, interpolation_engine). |