Usage

Convert dataclasses into a command-line interface.

Quickstart

To install, use

pip install clout

Define some dataclasses and convert them into a command-line interface.

import attr
import click

import clout


@attr.dataclass
class DB:
    host: str
    port: int


@attr.dataclass
class Config:
    db: DB
    dry_run: bool


cli = clout.Command(Config)

print(cli.build())
$ myapp --dry-run db --host example.com --port 9999
Config(db=DB(host='example.com', port=9999), dry_run=True)

A decorator

To run a function with the built object, you can use a decorator, clout.command(). Using clout.Command.main(), the program will exit immediately afterwards.

import attr

import clout


@attr.dataclass
class Person:
    name: str = attr.ib(metadata={"clout": {"cli": dict(envvar="EXAMPLE_NAME")}})
    age: int


@clout.command(Person)
def greet(person):
    print(f"Hello, {person.name}!")


if __name__ == "__main__":
    print(greet.main())
$ EXAMPLE_NAME=Alice python examples/decorator.py --age 21
Hello, Alice!

A callback

If you don’t want to use a decorator, you can use clout.Command() directly, and pass the callback= argument.

import attr
import click

import clout


@attr.dataclass
class Person:
    name: str
    age: int


def greet(person):
    print(f"Hello, {person.name}!")


cli = clout.Command(Person, callback=greet)

if __name__ == "__main__":
    cli.main()

Extended example

Now for a longer example.

  • Define a nested configuration object.
  • Load the object from the CLI, getting missing values from environment variables, configuration file, and dataclass default= values.

Here we define the config file in the Freedesktop standard directory.

# ~/.config/myapp/config.toml
[config]
dry_run=true

Set an environment variable and run the app.

% MYAPP_PRIORITY=2 python examples/long.py --debug  user --name Alice db --host example.com --port 9999 user --name Bob
Config(db=DB(host='example.com', port=9999, user=User(name='Bob')), debug=True, dry_run=True, user=User(name='Alice'), priority=2.0, logging=True)

The code:

import pathlib

import appdirs
import attr
import click
import toml

import clout


@attr.dataclass
class User:
    name: str


@attr.dataclass
class DB:
    host: str
    port: int
    user: User


@attr.dataclass
class Config:
    db: DB
    debug: bool
    dry_run: bool
    user: User
    priority: float = attr.ib(
        default=0,
        metadata={
            "clout": {
                "cli": dict(param_decls=["--priority"], help="App priority value")
            }
        },
    )
    logging: bool = True


APP_NAME = "myapp"


# Read config file.
with open("examples/config.toml") as f:
    CONFIG_FILE_DATA = toml.load(f)


# Read from environment_variables prefixed `MYAPP_`,
# such as MYAPP_PRIORITY=10.
ENVVAR_DATA = clout.load_env(Config, prefix=APP_NAME)


# Combine config file and envvars to set CLI defaults.
CONTEXT_SETTINGS = dict(default_map=clout.DeepChainMap(ENVVAR_DATA, CONFIG_FILE_DATA))

# Define the CLI.
commands = [
    clout.Command(
        name="config",
        type=Config,
        context_settings=CONTEXT_SETTINGS,
        help="Run the app with given configuration object.",
    )
]
cli = click.Group(commands={c.name: c for c in commands})


if __name__ == "__main__":
    # Run the CLI.

    print(cli.main(standalone_mode=False))