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.commandregisters a function and returns it unchanged, so it stays directly callable and testable. - Type hints drive parsing:
int,float,Path,boolflags,list[T],*args,Literal[...]andEnumchoices, andT | None. - Help from docstrings: the summary, description and Google-style
Args:entries become--helpoutput, with defaults and env vars listed. - Subcommand groups: nest apps with
app.group(...)or mount an existing one withapp.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/sechoemit ANSI color only to TTYs, and respectNO_COLORandFORCE_COLOR. - Clean errors and exit codes:
abort("msg")printsError: msgand exits non-zero, with no traceback. Return values become exit codes. - In-process testing:
slick_cli.testing.invokecaptures 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).helpdefaults to the docstring summary. Duplicate names or aliases raiseValueError.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 (usesother.nameifnameis omitted).app.run(argv=None) -> int: parseargv(defaultsys.argv[1:]), run the command, return the exit code. Never raisesSystemExit.app.main(argv=None)andapp(argv=None):run, thensys.exitwith the code.app.build_parser(prog=None) -> argparse.ArgumentParser: the underlying parser, if you need it.app.commands: dict of registeredCommandobjects and nestedApps, 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")]): ...
helpoverrides the docstring entry.shortadds a short flag (options and flags only).namereplaces the long option name.metavarsets the usage placeholder.envnames an environment variable used when the option is not given. Booleans accept1/true/yes/onand0/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 printsError: <message>to stderr.abort(message, exit_code=1): shorthand that raisesCliError.
Output
style(text, fg=None, bg=None, *, bold=False, dim=False, italic=False, underline=False) -> str: wrap text in ANSI codes. The colors areblack red green yellow blue magenta cyan white, each also with abright_prefix. An unknown color raisesValueError.unstyle(text) -> str: strip ANSI codes.echo(message="", *, err=False, nl=True, color=None): print to stdout or stderr. Styling is stripped unlessshould_color(stream)is true orcolor=Trueis passed.secho(message="", *, err=False, nl=True, color=None, **styles):styleandechoin one call.should_color(stream) -> bool: returnsFalseifNO_COLORis set, otherwiseTrueifFORCE_COLORis set, otherwise whetherstreamis a TTY.confirm(prompt, default=False) -> bool: a yes/no prompt on stdin. An empty answer or end of input returnsdefault.
Lower-level pieces
Command(func, name=None, *, help=None, aliases=()): the object behind each registered command. It has.name,.help,.description,.aliasesand.params.slick_cli.docstrings.parse_docstring(doc) -> DocInforeturnssummary,descriptionandparams.
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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| slick_cli-0.1.0.tar.gz | 22.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.