Skip to main content

slick-cli

Write a function, get a command-line app. slick-cli turns ordinary, type-hinted Python functions into polished CLIs: arguments, options, flags, choices, subcommands, help text and colored output, with zero dependencies (it is a thin, friendly layer over argparse).

from slick_cli import App, echo

app = App("greeter", "Friendly greetings.", version="1.0")

@app.command
def hello(name: str, count: int = 1, shout: bool = False) -> None:
    """Greet someone.

    Args:
        name: Who to greet.
        count: How many times.
        shout: Use UPPERCASE.
    """
    for _ in range(count):
        echo(name.upper() if shout else f"Hello, {name}!")

if __name__ == "__main__":
    app()
$ python greeter.py hello Ada --count 2
Hello, Ada!
Hello, Ada!
$ python greeter.py hello --help
usage: greeter hello [-h] [--count COUNT] [--shout | --no-shout] NAME
...

Features

  • Decorator-based commands: @app.command registers a function and returns it unchanged, so it stays directly callable and testable.
  • Type hints drive parsing: int, float, Path, bool flags, list[T], *args, Literal[...] and Enum choices, and T | None.
  • Help from docstrings: the summary, description and Google-style Args: entries become --help output, with defaults and env vars listed.
  • Subcommand groups: nest apps with app.group(...) or mount an existing one with app.add_app(...); command aliases are supported.
  • Per-parameter tweaks via Annotated[T, Arg(...)]: short flags, custom names, metavars and environment-variable fallbacks.
  • Colored output that behaves: style/secho emit ANSI color only to TTYs, and respect NO_COLOR and FORCE_COLOR.
  • Clean errors and exit codes: abort("msg") prints Error: msg and exits non-zero, with no traceback. Return values become exit codes.
  • In-process testing: slick_cli.testing.invoke captures exit code, stdout and stderr.
  • Standard library only, Python 3.10+, fully typed (py.typed).

Install

pip install slick-cli

Or from a checkout: pip install .

Quickstart

# todo.py
from pathlib import Path
from typing import Annotated, Literal

from slick_cli import App, Arg, abort, secho

app = App("todo", "A tiny todo manager.")
FILE = Path("todo.txt")

@app.command
def add(text: list[str], *, priority: Literal["low", "high"] = "low") -> None:
    """Add an item."""
    with FILE.open("a") as f:
        f.write(f"[{priority}] {' '.join(text)}\n")
    secho("added", fg="green")

@app.command(name="list", aliases=["ls"])
def list_items(*, high_only: Annotated[bool, Arg(short="-H")] = False) -> None:
    """Show items."""
    if not FILE.exists():
        abort("nothing to do yet")
    for line in FILE.read_text().splitlines():
        if not high_only or line.startswith("[high]"):
            print(line)

if __name__ == "__main__":
    app()
$ python todo.py add buy milk --priority high
added
$ python todo.py ls -H
[high] buy milk

A runnable tour lives in examples/demo.py:

python3 examples/demo.py --help
python3 examples/demo.py hello Ada --count 2 --shout --color magenta
python3 examples/demo.py sum 1.5 2 3.25 -p 1
python3 examples/demo.py files ls --path . --ext .py
DEMO_TOKEN=secret python3 examples/demo.py deploy prod -y

How parameters map to the command line

Python parameter Command line
name: str (no default) positional NAME
count: int = 1 (has a default) option --count COUNT
*, user: str (keyword-only, no default) required option --user USER
verbose: bool = False flag --verbose / --no-verbose
files: list[Path] (no default) positional taking one or more values
*, tag: list[str] | None = None repeatable option --tag a --tag b
*rest: int positional taking zero or more values
mode: Literal["fast", "slow"] choices {fast,slow}
color: Color (an Enum) choices from member values (names also accepted, case-insensitive)
out: Path | None = None same as Path; None when omitted
when: SomeType SomeType(string) is used as the converter

Underscores in names become dashes (dry_run becomes --dry-run). Unannotated parameters are strings. A bool with no default is a flag defaulting to False. **kwargs is rejected with TypeError.

API overview

Everything below is importable from slick_cli unless noted.

App(name=None, help=None, *, version=None)

A collection of commands and nested groups.

  • @app.command / @app.command(name=None, *, help=None, aliases=()): register a function. The name defaults to the function name with _ replaced by - (leading/trailing underscores stripped). help defaults to the docstring summary. Duplicate names or aliases raise ValueError.
  • app.group(name, help=None) -> App: create and mount a nested app for subcommands.
  • app.add_app(other, name=None) -> App: mount an existing app (uses other.name if name is omitted).
  • app.run(argv=None) -> int: parse argv (default sys.argv[1:]), run the command, return the exit code. Never raises SystemExit.
  • app.main(argv=None) and app(argv=None): run, then sys.exit with the code.
  • app.build_parser(prog=None) -> argparse.ArgumentParser: the underlying parser, if you need it.
  • app.commands: dict of registered Command objects and nested Apps, by name.

Running an app (or a group) with no command prints its help and exits 0. --version is added when version is set.

run(func, argv=None, *, prog=None, version=None) -> int

Run a single function as a whole CLI with no subcommands.

Exit codes

Outcome Exit code
command returns None 0
command returns an int that int
command returns True / False 0 / 1
command returns anything else printed, 0
CliError(msg, code) / abort(msg, code) code (default 1)
usage error (bad value, missing argument) 2
Ctrl-C 130

Arg(help=None, short=None, name=None, metavar=None, env=None)

Per-parameter metadata, attached with typing.Annotated:

def login(*, user: Annotated[str, Arg(short="-u", env="APP_USER", help="Account name")]): ...
  • help overrides the docstring entry.
  • short adds a short flag (options and flags only).
  • name replaces the long option name.
  • metavar sets the usage placeholder.
  • env names an environment variable used when the option is not given. Booleans accept 1/true/yes/on and 0/false/no/off. List options split the variable on commas. A bad value is a usage error (exit 2).

Errors

  • CliError(message, exit_code=1): raise it for expected failures. It prints Error: <message> to stderr.
  • abort(message, exit_code=1): shorthand that raises CliError.

Output

  • style(text, fg=None, bg=None, *, bold=False, dim=False, italic=False, underline=False) -> str: wrap text in ANSI codes. The colors are black red green yellow blue magenta cyan white, each also with a bright_ prefix. An unknown color raises ValueError.
  • unstyle(text) -> str: strip ANSI codes.
  • echo(message="", *, err=False, nl=True, color=None): print to stdout or stderr. Styling is stripped unless should_color(stream) is true or color=True is passed.
  • secho(message="", *, err=False, nl=True, color=None, **styles): style and echo in one call.
  • should_color(stream) -> bool: returns False if NO_COLOR is set, otherwise True if FORCE_COLOR is set, otherwise whether stream is a TTY.
  • confirm(prompt, default=False) -> bool: a yes/no prompt on stdin. An empty answer or end of input returns default.

Lower-level pieces

  • Command(func, name=None, *, help=None, aliases=()): the object behind each registered command. It has .name, .help, .description, .aliases and .params.
  • slick_cli.docstrings.parse_docstring(doc) -> DocInfo returns summary, description and params.

Testing: slick_cli.testing

from slick_cli.testing import invoke

result = invoke(app, ["hello", "Ada"], env={"APP_USER": "me"}, input="y\n")
assert result.exit_code == 0
assert result.stdout == "Hello, Ada!\n"

invoke(target, args=(), *, env=None, input=None) -> Result accepts an App or a plain function. Result has exit_code, stdout and stderr.

Development

PYTHONPATH=src python3 -m unittest discover -s tests -v
# or, if pytest is installed:
python3 -m pytest

License

MIT

Metadata

Release files for slick-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for slick-cli 0.1.0
File Size Uploaded Provenance
slick_cli-0.1.0.tar.gz 22.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for slick-cli 0.1.0
File Interpreter ABI Platform
slick_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.8 kB

Release files / slick_cli-0.1.0.tar.gz

Download URL slick_cli-0.1.0.tar.gz
Size 22.5 kB
Tags Source
SHA-256 checksum
How to use checksums
409cf1d63e7e80a5f5f83d43f0da78167cf0d4fa531abf7efa709d584c4765b4
BLAKE2b-256 checksum
How to use checksums
6bb125682edf862ab61742822904add85abaf99651226f1f4b5058832e378bbd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / slick_cli-0.1.0-py3-none-any.whl

Download URL slick_cli-0.1.0-py3-none-any.whl
Size 18.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9bb5c9b8ed01788c25c2ec02d7c9084b01b66a1d05b4d8e3797df78a6bc73eac
BLAKE2b-256 checksum
How to use checksums
96ba52d1353547854f789205f4f2899e33a0057953e7938431093cd51a381b96
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.5

Security for slick-cli 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

0.0.1

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page