Custom CLI Source#
CliSource is the abstract base class for all CLI sources. To plug in a different CLI library (click, typer, anything else), subclass it and implement one method: _parse_argv() -> dict[str, JSONValue].
For the built-in argparse integration, see ArgparseSource.
The _parse_argv contract#
- Top-level args → key = field name.
- Groups / subcommands → emit a discriminator key + prefix the group's args with the chosen group name, joined with
self.nested_sep. - Bool-style flags — always in the result.
- Non-bool args — only if the user explicitly passed them.
- The parser/library reads
sys.argvitself; do not add anargv=parameter.
ClickSource example#
Below is a complete ClickSource you can copy into your project. It supports click groups of arbitrary depth.
import sys
from dataclasses import dataclass
from typing import ClassVar
import click
import dature
from dature.sources.base import CliSource
@dataclass(kw_only=True, repr=False)
class ClickSource(CliSource):
cli: click.Command
discriminator: str = "command"
format_name: ClassVar[str] = "click"
def _parse_argv(self) -> dict[str, dature.type_aliases.JSONValue]:
ctx = self.cli.make_context(
info_name=self.cli.name or "cli",
args=sys.argv[1:],
resilient_parsing=False,
)
out: dict[str, dature.type_aliases.JSONValue] = {}
self._walk(ctx, self.cli, prefix=[], out=out)
return out
def _walk(
self,
ctx: click.Context,
cmd: click.Command,
*,
prefix: list[str],
out: dict[str, dature.type_aliases.JSONValue],
) -> None:
sep = self.nested_sep
for param in cmd.params:
param_name = param.name or ""
value = ctx.params.get(param_name)
source = ctx.get_parameter_source(param_name)
key = sep.join([*prefix, param_name])
if (
isinstance(param, click.Option) and param.is_flag
) or source == click.core.ParameterSource.COMMANDLINE:
out[key] = value
if not isinstance(cmd, click.Group):
return
rest = [*getattr(ctx, "protected_args", ()), *ctx.args]
if not rest:
return
sub_name, sub_cmd, sub_args = cmd.resolve_command(ctx, rest)
if sub_cmd is None or sub_name is None:
return
out[sep.join([*prefix, self.discriminator])] = sub_name
sub_ctx = sub_cmd.make_context(
info_name=sub_name,
args=sub_args,
parent=ctx,
resilient_parsing=False,
)
self._walk(sub_ctx, sub_cmd, prefix=[*prefix, sub_name], out=out)
@click.group(invoke_without_command=True)
@click.option("--verbose", is_flag=True)
def cli(verbose: bool) -> None: # noqa: FBT001
pass
@cli.command()
@click.option("--name", required=False)
def create(name: str | None) -> None:
pass
@cli.command()
@click.option("--item-id", type=int, required=False)
def delete(item_id: int | None) -> None:
pass
@dataclass
class CreateArgs:
name: str = "default"
@dataclass
class DeleteArgs:
item_id: int = 0
@dataclass
class Config:
command: str | None = None
verbose: bool = False
create: CreateArgs | None = None
delete: DeleteArgs | None = None
def main() -> None:
config = dature.load(ClickSource(cli=cli), schema=Config)
print(config)
if __name__ == "__main__":
main()
A TyperSource is a thin wrapper — typer commands are click commands under the hood, so subclassing ClickSource and pointing at the underlying click group works directly.
Not part of dature's API surface
ClickSource above is a teaching example. It's not shipped, not tested by dature's CI, and not bound by dature's backward-compatibility guarantees. Treat it as a starting point for your own implementation.