slick-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slick_cli-0.1.0/LICENSE +21 -0
- slick_cli-0.1.0/PKG-INFO +243 -0
- slick_cli-0.1.0/README.md +218 -0
- slick_cli-0.1.0/pyproject.toml +40 -0
- slick_cli-0.1.0/setup.cfg +4 -0
- slick_cli-0.1.0/src/slick_cli/__init__.py +40 -0
- slick_cli-0.1.0/src/slick_cli/app.py +255 -0
- slick_cli-0.1.0/src/slick_cli/docstrings.py +81 -0
- slick_cli-0.1.0/src/slick_cli/errors.py +29 -0
- slick_cli-0.1.0/src/slick_cli/params.py +295 -0
- slick_cli-0.1.0/src/slick_cli/py.typed +0 -0
- slick_cli-0.1.0/src/slick_cli/style.py +166 -0
- slick_cli-0.1.0/src/slick_cli/testing.py +51 -0
- slick_cli-0.1.0/src/slick_cli.egg-info/PKG-INFO +243 -0
- slick_cli-0.1.0/src/slick_cli.egg-info/SOURCES.txt +17 -0
- slick_cli-0.1.0/src/slick_cli.egg-info/dependency_links.txt +1 -0
- slick_cli-0.1.0/src/slick_cli.egg-info/top_level.txt +1 -0
- slick_cli-0.1.0/tests/test_app.py +258 -0
- slick_cli-0.1.0/tests/test_support.py +82 -0
slick_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nehz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
slick_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slick-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Slick, type-hint driven command-line apps built on the Python standard library.
|
|
5
|
+
Author: nehz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: cli,command-line,argparse,decorator,type-hints,terminal
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# slick-cli
|
|
27
|
+
|
|
28
|
+
**Write a function, get a command-line app.** `slick-cli` turns ordinary, type-hinted
|
|
29
|
+
Python functions into polished CLIs: arguments, options, flags, choices, subcommands,
|
|
30
|
+
help text and colored output, with **zero dependencies** (it is a thin, friendly layer
|
|
31
|
+
over `argparse`).
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from slick_cli import App, echo
|
|
35
|
+
|
|
36
|
+
app = App("greeter", "Friendly greetings.", version="1.0")
|
|
37
|
+
|
|
38
|
+
@app.command
|
|
39
|
+
def hello(name: str, count: int = 1, shout: bool = False) -> None:
|
|
40
|
+
"""Greet someone.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
name: Who to greet.
|
|
44
|
+
count: How many times.
|
|
45
|
+
shout: Use UPPERCASE.
|
|
46
|
+
"""
|
|
47
|
+
for _ in range(count):
|
|
48
|
+
echo(name.upper() if shout else f"Hello, {name}!")
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
app()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```console
|
|
55
|
+
$ python greeter.py hello Ada --count 2
|
|
56
|
+
Hello, Ada!
|
|
57
|
+
Hello, Ada!
|
|
58
|
+
$ python greeter.py hello --help
|
|
59
|
+
usage: greeter hello [-h] [--count COUNT] [--shout | --no-shout] NAME
|
|
60
|
+
...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **Decorator-based commands**: `@app.command` registers a function and returns it unchanged, so it stays directly callable and testable.
|
|
66
|
+
- **Type hints drive parsing**: `int`, `float`, `Path`, `bool` flags, `list[T]`, `*args`, `Literal[...]` and `Enum` choices, and `T | None`.
|
|
67
|
+
- **Help from docstrings**: the summary, description and Google-style `Args:` entries become `--help` output, with defaults and env vars listed.
|
|
68
|
+
- **Subcommand groups**: nest apps with `app.group(...)` or mount an existing one with `app.add_app(...)`; command aliases are supported.
|
|
69
|
+
- **Per-parameter tweaks** via `Annotated[T, Arg(...)]`: short flags, custom names, metavars and environment-variable fallbacks.
|
|
70
|
+
- **Colored output that behaves**: `style`/`secho` emit ANSI color only to TTYs, and respect `NO_COLOR` and `FORCE_COLOR`.
|
|
71
|
+
- **Clean errors and exit codes**: `abort("msg")` prints `Error: msg` and exits non-zero, with no traceback. Return values become exit codes.
|
|
72
|
+
- **In-process testing**: `slick_cli.testing.invoke` captures exit code, stdout and stderr.
|
|
73
|
+
- Standard library only, Python 3.10+, fully typed (`py.typed`).
|
|
74
|
+
|
|
75
|
+
## Install
|
|
76
|
+
|
|
77
|
+
```console
|
|
78
|
+
pip install slick-cli
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Or from a checkout: `pip install .`
|
|
82
|
+
|
|
83
|
+
## Quickstart
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# todo.py
|
|
87
|
+
from pathlib import Path
|
|
88
|
+
from typing import Annotated, Literal
|
|
89
|
+
|
|
90
|
+
from slick_cli import App, Arg, abort, secho
|
|
91
|
+
|
|
92
|
+
app = App("todo", "A tiny todo manager.")
|
|
93
|
+
FILE = Path("todo.txt")
|
|
94
|
+
|
|
95
|
+
@app.command
|
|
96
|
+
def add(text: list[str], *, priority: Literal["low", "high"] = "low") -> None:
|
|
97
|
+
"""Add an item."""
|
|
98
|
+
with FILE.open("a") as f:
|
|
99
|
+
f.write(f"[{priority}] {' '.join(text)}\n")
|
|
100
|
+
secho("added", fg="green")
|
|
101
|
+
|
|
102
|
+
@app.command(name="list", aliases=["ls"])
|
|
103
|
+
def list_items(*, high_only: Annotated[bool, Arg(short="-H")] = False) -> None:
|
|
104
|
+
"""Show items."""
|
|
105
|
+
if not FILE.exists():
|
|
106
|
+
abort("nothing to do yet")
|
|
107
|
+
for line in FILE.read_text().splitlines():
|
|
108
|
+
if not high_only or line.startswith("[high]"):
|
|
109
|
+
print(line)
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
app()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```console
|
|
116
|
+
$ python todo.py add buy milk --priority high
|
|
117
|
+
added
|
|
118
|
+
$ python todo.py ls -H
|
|
119
|
+
[high] buy milk
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A runnable tour lives in [`examples/demo.py`](examples/demo.py):
|
|
123
|
+
|
|
124
|
+
```console
|
|
125
|
+
python3 examples/demo.py --help
|
|
126
|
+
python3 examples/demo.py hello Ada --count 2 --shout --color magenta
|
|
127
|
+
python3 examples/demo.py sum 1.5 2 3.25 -p 1
|
|
128
|
+
python3 examples/demo.py files ls --path . --ext .py
|
|
129
|
+
DEMO_TOKEN=secret python3 examples/demo.py deploy prod -y
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## How parameters map to the command line
|
|
133
|
+
|
|
134
|
+
| Python parameter | Command line |
|
|
135
|
+
|----------------------------------------|------------------------------------------------|
|
|
136
|
+
| `name: str` (no default) | positional `NAME` |
|
|
137
|
+
| `count: int = 1` (has a default) | option `--count COUNT` |
|
|
138
|
+
| `*, user: str` (keyword-only, no default) | required option `--user USER` |
|
|
139
|
+
| `verbose: bool = False` | flag `--verbose / --no-verbose` |
|
|
140
|
+
| `files: list[Path]` (no default) | positional taking one or more values |
|
|
141
|
+
| `*, tag: list[str] \| None = None` | repeatable option `--tag a --tag b` |
|
|
142
|
+
| `*rest: int` | positional taking zero or more values |
|
|
143
|
+
| `mode: Literal["fast", "slow"]` | choices `{fast,slow}` |
|
|
144
|
+
| `color: Color` (an `Enum`) | choices from member values (names also accepted, case-insensitive) |
|
|
145
|
+
| `out: Path \| None = None` | same as `Path`; `None` when omitted |
|
|
146
|
+
| `when: SomeType` | `SomeType(string)` is used as the converter |
|
|
147
|
+
|
|
148
|
+
Underscores in names become dashes (`dry_run` becomes `--dry-run`). Unannotated
|
|
149
|
+
parameters are strings. A `bool` with no default is a flag defaulting to `False`.
|
|
150
|
+
`**kwargs` is rejected with `TypeError`.
|
|
151
|
+
|
|
152
|
+
## API overview
|
|
153
|
+
|
|
154
|
+
Everything below is importable from `slick_cli` unless noted.
|
|
155
|
+
|
|
156
|
+
### `App(name=None, help=None, *, version=None)`
|
|
157
|
+
|
|
158
|
+
A collection of commands and nested groups.
|
|
159
|
+
|
|
160
|
+
- `@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`.
|
|
161
|
+
- `app.group(name, help=None) -> App`: create and mount a nested app for subcommands.
|
|
162
|
+
- `app.add_app(other, name=None) -> App`: mount an existing app (uses `other.name` if `name` is omitted).
|
|
163
|
+
- `app.run(argv=None) -> int`: parse `argv` (default `sys.argv[1:]`), run the command, return the exit code. Never raises `SystemExit`.
|
|
164
|
+
- `app.main(argv=None)` and `app(argv=None)`: `run`, then `sys.exit` with the code.
|
|
165
|
+
- `app.build_parser(prog=None) -> argparse.ArgumentParser`: the underlying parser, if you need it.
|
|
166
|
+
- `app.commands`: dict of registered `Command` objects and nested `App`s, by name.
|
|
167
|
+
|
|
168
|
+
Running an app (or a group) with no command prints its help and exits 0.
|
|
169
|
+
`--version` is added when `version` is set.
|
|
170
|
+
|
|
171
|
+
### `run(func, argv=None, *, prog=None, version=None) -> int`
|
|
172
|
+
|
|
173
|
+
Run a single function as a whole CLI with no subcommands.
|
|
174
|
+
|
|
175
|
+
### Exit codes
|
|
176
|
+
|
|
177
|
+
| Outcome | Exit code |
|
|
178
|
+
|-------------------------------------------------|----------------|
|
|
179
|
+
| command returns `None` | `0` |
|
|
180
|
+
| command returns an `int` | that int |
|
|
181
|
+
| command returns `True` / `False` | `0` / `1` |
|
|
182
|
+
| command returns anything else | printed, `0` |
|
|
183
|
+
| `CliError(msg, code)` / `abort(msg, code)` | `code` (default `1`) |
|
|
184
|
+
| usage error (bad value, missing argument) | `2` |
|
|
185
|
+
| `Ctrl-C` | `130` |
|
|
186
|
+
|
|
187
|
+
### `Arg(help=None, short=None, name=None, metavar=None, env=None)`
|
|
188
|
+
|
|
189
|
+
Per-parameter metadata, attached with `typing.Annotated`:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
def login(*, user: Annotated[str, Arg(short="-u", env="APP_USER", help="Account name")]): ...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- `help` overrides the docstring entry.
|
|
196
|
+
- `short` adds a short flag (options and flags only).
|
|
197
|
+
- `name` replaces the long option name.
|
|
198
|
+
- `metavar` sets the usage placeholder.
|
|
199
|
+
- `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).
|
|
200
|
+
|
|
201
|
+
### Errors
|
|
202
|
+
|
|
203
|
+
- `CliError(message, exit_code=1)`: raise it for expected failures. It prints `Error: <message>` to stderr.
|
|
204
|
+
- `abort(message, exit_code=1)`: shorthand that raises `CliError`.
|
|
205
|
+
|
|
206
|
+
### Output
|
|
207
|
+
|
|
208
|
+
- `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`.
|
|
209
|
+
- `unstyle(text) -> str`: strip ANSI codes.
|
|
210
|
+
- `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.
|
|
211
|
+
- `secho(message="", *, err=False, nl=True, color=None, **styles)`: `style` and `echo` in one call.
|
|
212
|
+
- `should_color(stream) -> bool`: returns `False` if `NO_COLOR` is set, otherwise `True` if `FORCE_COLOR` is set, otherwise whether `stream` is a TTY.
|
|
213
|
+
- `confirm(prompt, default=False) -> bool`: a yes/no prompt on stdin. An empty answer or end of input returns `default`.
|
|
214
|
+
|
|
215
|
+
### Lower-level pieces
|
|
216
|
+
|
|
217
|
+
- `Command(func, name=None, *, help=None, aliases=())`: the object behind each registered command. It has `.name`, `.help`, `.description`, `.aliases` and `.params`.
|
|
218
|
+
- `slick_cli.docstrings.parse_docstring(doc) -> DocInfo` returns `summary`, `description` and `params`.
|
|
219
|
+
|
|
220
|
+
### Testing: `slick_cli.testing`
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from slick_cli.testing import invoke
|
|
224
|
+
|
|
225
|
+
result = invoke(app, ["hello", "Ada"], env={"APP_USER": "me"}, input="y\n")
|
|
226
|
+
assert result.exit_code == 0
|
|
227
|
+
assert result.stdout == "Hello, Ada!\n"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`invoke(target, args=(), *, env=None, input=None) -> Result` accepts an `App` or a plain
|
|
231
|
+
function. `Result` has `exit_code`, `stdout` and `stderr`.
|
|
232
|
+
|
|
233
|
+
## Development
|
|
234
|
+
|
|
235
|
+
```console
|
|
236
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
237
|
+
# or, if pytest is installed:
|
|
238
|
+
python3 -m pytest
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
MIT
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# slick-cli
|
|
2
|
+
|
|
3
|
+
**Write a function, get a command-line app.** `slick-cli` turns ordinary, type-hinted
|
|
4
|
+
Python functions into polished CLIs: arguments, options, flags, choices, subcommands,
|
|
5
|
+
help text and colored output, with **zero dependencies** (it is a thin, friendly layer
|
|
6
|
+
over `argparse`).
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
from slick_cli import App, echo
|
|
10
|
+
|
|
11
|
+
app = App("greeter", "Friendly greetings.", version="1.0")
|
|
12
|
+
|
|
13
|
+
@app.command
|
|
14
|
+
def hello(name: str, count: int = 1, shout: bool = False) -> None:
|
|
15
|
+
"""Greet someone.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
name: Who to greet.
|
|
19
|
+
count: How many times.
|
|
20
|
+
shout: Use UPPERCASE.
|
|
21
|
+
"""
|
|
22
|
+
for _ in range(count):
|
|
23
|
+
echo(name.upper() if shout else f"Hello, {name}!")
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
app()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```console
|
|
30
|
+
$ python greeter.py hello Ada --count 2
|
|
31
|
+
Hello, Ada!
|
|
32
|
+
Hello, Ada!
|
|
33
|
+
$ python greeter.py hello --help
|
|
34
|
+
usage: greeter hello [-h] [--count COUNT] [--shout | --no-shout] NAME
|
|
35
|
+
...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Decorator-based commands**: `@app.command` registers a function and returns it unchanged, so it stays directly callable and testable.
|
|
41
|
+
- **Type hints drive parsing**: `int`, `float`, `Path`, `bool` flags, `list[T]`, `*args`, `Literal[...]` and `Enum` choices, and `T | None`.
|
|
42
|
+
- **Help from docstrings**: the summary, description and Google-style `Args:` entries become `--help` output, with defaults and env vars listed.
|
|
43
|
+
- **Subcommand groups**: nest apps with `app.group(...)` or mount an existing one with `app.add_app(...)`; command aliases are supported.
|
|
44
|
+
- **Per-parameter tweaks** via `Annotated[T, Arg(...)]`: short flags, custom names, metavars and environment-variable fallbacks.
|
|
45
|
+
- **Colored output that behaves**: `style`/`secho` emit ANSI color only to TTYs, and respect `NO_COLOR` and `FORCE_COLOR`.
|
|
46
|
+
- **Clean errors and exit codes**: `abort("msg")` prints `Error: msg` and exits non-zero, with no traceback. Return values become exit codes.
|
|
47
|
+
- **In-process testing**: `slick_cli.testing.invoke` captures exit code, stdout and stderr.
|
|
48
|
+
- Standard library only, Python 3.10+, fully typed (`py.typed`).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```console
|
|
53
|
+
pip install slick-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or from a checkout: `pip install .`
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# todo.py
|
|
62
|
+
from pathlib import Path
|
|
63
|
+
from typing import Annotated, Literal
|
|
64
|
+
|
|
65
|
+
from slick_cli import App, Arg, abort, secho
|
|
66
|
+
|
|
67
|
+
app = App("todo", "A tiny todo manager.")
|
|
68
|
+
FILE = Path("todo.txt")
|
|
69
|
+
|
|
70
|
+
@app.command
|
|
71
|
+
def add(text: list[str], *, priority: Literal["low", "high"] = "low") -> None:
|
|
72
|
+
"""Add an item."""
|
|
73
|
+
with FILE.open("a") as f:
|
|
74
|
+
f.write(f"[{priority}] {' '.join(text)}\n")
|
|
75
|
+
secho("added", fg="green")
|
|
76
|
+
|
|
77
|
+
@app.command(name="list", aliases=["ls"])
|
|
78
|
+
def list_items(*, high_only: Annotated[bool, Arg(short="-H")] = False) -> None:
|
|
79
|
+
"""Show items."""
|
|
80
|
+
if not FILE.exists():
|
|
81
|
+
abort("nothing to do yet")
|
|
82
|
+
for line in FILE.read_text().splitlines():
|
|
83
|
+
if not high_only or line.startswith("[high]"):
|
|
84
|
+
print(line)
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
app()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```console
|
|
91
|
+
$ python todo.py add buy milk --priority high
|
|
92
|
+
added
|
|
93
|
+
$ python todo.py ls -H
|
|
94
|
+
[high] buy milk
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A runnable tour lives in [`examples/demo.py`](examples/demo.py):
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
python3 examples/demo.py --help
|
|
101
|
+
python3 examples/demo.py hello Ada --count 2 --shout --color magenta
|
|
102
|
+
python3 examples/demo.py sum 1.5 2 3.25 -p 1
|
|
103
|
+
python3 examples/demo.py files ls --path . --ext .py
|
|
104
|
+
DEMO_TOKEN=secret python3 examples/demo.py deploy prod -y
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## How parameters map to the command line
|
|
108
|
+
|
|
109
|
+
| Python parameter | Command line |
|
|
110
|
+
|----------------------------------------|------------------------------------------------|
|
|
111
|
+
| `name: str` (no default) | positional `NAME` |
|
|
112
|
+
| `count: int = 1` (has a default) | option `--count COUNT` |
|
|
113
|
+
| `*, user: str` (keyword-only, no default) | required option `--user USER` |
|
|
114
|
+
| `verbose: bool = False` | flag `--verbose / --no-verbose` |
|
|
115
|
+
| `files: list[Path]` (no default) | positional taking one or more values |
|
|
116
|
+
| `*, tag: list[str] \| None = None` | repeatable option `--tag a --tag b` |
|
|
117
|
+
| `*rest: int` | positional taking zero or more values |
|
|
118
|
+
| `mode: Literal["fast", "slow"]` | choices `{fast,slow}` |
|
|
119
|
+
| `color: Color` (an `Enum`) | choices from member values (names also accepted, case-insensitive) |
|
|
120
|
+
| `out: Path \| None = None` | same as `Path`; `None` when omitted |
|
|
121
|
+
| `when: SomeType` | `SomeType(string)` is used as the converter |
|
|
122
|
+
|
|
123
|
+
Underscores in names become dashes (`dry_run` becomes `--dry-run`). Unannotated
|
|
124
|
+
parameters are strings. A `bool` with no default is a flag defaulting to `False`.
|
|
125
|
+
`**kwargs` is rejected with `TypeError`.
|
|
126
|
+
|
|
127
|
+
## API overview
|
|
128
|
+
|
|
129
|
+
Everything below is importable from `slick_cli` unless noted.
|
|
130
|
+
|
|
131
|
+
### `App(name=None, help=None, *, version=None)`
|
|
132
|
+
|
|
133
|
+
A collection of commands and nested groups.
|
|
134
|
+
|
|
135
|
+
- `@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`.
|
|
136
|
+
- `app.group(name, help=None) -> App`: create and mount a nested app for subcommands.
|
|
137
|
+
- `app.add_app(other, name=None) -> App`: mount an existing app (uses `other.name` if `name` is omitted).
|
|
138
|
+
- `app.run(argv=None) -> int`: parse `argv` (default `sys.argv[1:]`), run the command, return the exit code. Never raises `SystemExit`.
|
|
139
|
+
- `app.main(argv=None)` and `app(argv=None)`: `run`, then `sys.exit` with the code.
|
|
140
|
+
- `app.build_parser(prog=None) -> argparse.ArgumentParser`: the underlying parser, if you need it.
|
|
141
|
+
- `app.commands`: dict of registered `Command` objects and nested `App`s, by name.
|
|
142
|
+
|
|
143
|
+
Running an app (or a group) with no command prints its help and exits 0.
|
|
144
|
+
`--version` is added when `version` is set.
|
|
145
|
+
|
|
146
|
+
### `run(func, argv=None, *, prog=None, version=None) -> int`
|
|
147
|
+
|
|
148
|
+
Run a single function as a whole CLI with no subcommands.
|
|
149
|
+
|
|
150
|
+
### Exit codes
|
|
151
|
+
|
|
152
|
+
| Outcome | Exit code |
|
|
153
|
+
|-------------------------------------------------|----------------|
|
|
154
|
+
| command returns `None` | `0` |
|
|
155
|
+
| command returns an `int` | that int |
|
|
156
|
+
| command returns `True` / `False` | `0` / `1` |
|
|
157
|
+
| command returns anything else | printed, `0` |
|
|
158
|
+
| `CliError(msg, code)` / `abort(msg, code)` | `code` (default `1`) |
|
|
159
|
+
| usage error (bad value, missing argument) | `2` |
|
|
160
|
+
| `Ctrl-C` | `130` |
|
|
161
|
+
|
|
162
|
+
### `Arg(help=None, short=None, name=None, metavar=None, env=None)`
|
|
163
|
+
|
|
164
|
+
Per-parameter metadata, attached with `typing.Annotated`:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
def login(*, user: Annotated[str, Arg(short="-u", env="APP_USER", help="Account name")]): ...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- `help` overrides the docstring entry.
|
|
171
|
+
- `short` adds a short flag (options and flags only).
|
|
172
|
+
- `name` replaces the long option name.
|
|
173
|
+
- `metavar` sets the usage placeholder.
|
|
174
|
+
- `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).
|
|
175
|
+
|
|
176
|
+
### Errors
|
|
177
|
+
|
|
178
|
+
- `CliError(message, exit_code=1)`: raise it for expected failures. It prints `Error: <message>` to stderr.
|
|
179
|
+
- `abort(message, exit_code=1)`: shorthand that raises `CliError`.
|
|
180
|
+
|
|
181
|
+
### Output
|
|
182
|
+
|
|
183
|
+
- `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`.
|
|
184
|
+
- `unstyle(text) -> str`: strip ANSI codes.
|
|
185
|
+
- `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.
|
|
186
|
+
- `secho(message="", *, err=False, nl=True, color=None, **styles)`: `style` and `echo` in one call.
|
|
187
|
+
- `should_color(stream) -> bool`: returns `False` if `NO_COLOR` is set, otherwise `True` if `FORCE_COLOR` is set, otherwise whether `stream` is a TTY.
|
|
188
|
+
- `confirm(prompt, default=False) -> bool`: a yes/no prompt on stdin. An empty answer or end of input returns `default`.
|
|
189
|
+
|
|
190
|
+
### Lower-level pieces
|
|
191
|
+
|
|
192
|
+
- `Command(func, name=None, *, help=None, aliases=())`: the object behind each registered command. It has `.name`, `.help`, `.description`, `.aliases` and `.params`.
|
|
193
|
+
- `slick_cli.docstrings.parse_docstring(doc) -> DocInfo` returns `summary`, `description` and `params`.
|
|
194
|
+
|
|
195
|
+
### Testing: `slick_cli.testing`
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from slick_cli.testing import invoke
|
|
199
|
+
|
|
200
|
+
result = invoke(app, ["hello", "Ada"], env={"APP_USER": "me"}, input="y\n")
|
|
201
|
+
assert result.exit_code == 0
|
|
202
|
+
assert result.stdout == "Hello, Ada!\n"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`invoke(target, args=(), *, env=None, input=None) -> Result` accepts an `App` or a plain
|
|
206
|
+
function. `Result` has `exit_code`, `stdout` and `stderr`.
|
|
207
|
+
|
|
208
|
+
## Development
|
|
209
|
+
|
|
210
|
+
```console
|
|
211
|
+
PYTHONPATH=src python3 -m unittest discover -s tests -v
|
|
212
|
+
# or, if pytest is installed:
|
|
213
|
+
python3 -m pytest
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "slick-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Slick, type-hint driven command-line apps built on the Python standard library."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "nehz" }]
|
|
14
|
+
keywords = ["cli", "command-line", "argparse", "decorator", "type-hints", "terminal"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Software Development :: User Interfaces",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
slick_cli = ["py.typed"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""slick_cli: ergonomic, type-hint driven command-line apps on the standard library.
|
|
2
|
+
|
|
3
|
+
Quick example::
|
|
4
|
+
|
|
5
|
+
from slick_cli import App, echo
|
|
6
|
+
|
|
7
|
+
app = App("hello")
|
|
8
|
+
|
|
9
|
+
@app.command
|
|
10
|
+
def greet(name: str, count: int = 1, shout: bool = False) -> None:
|
|
11
|
+
'''Greet someone.'''
|
|
12
|
+
for _ in range(count):
|
|
13
|
+
echo(name.upper() if shout else name)
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
app()
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .app import App, Command, run
|
|
20
|
+
from .errors import CliError, abort
|
|
21
|
+
from .params import Arg
|
|
22
|
+
from .style import confirm, echo, secho, should_color, style, unstyle
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"App",
|
|
28
|
+
"Arg",
|
|
29
|
+
"CliError",
|
|
30
|
+
"Command",
|
|
31
|
+
"abort",
|
|
32
|
+
"confirm",
|
|
33
|
+
"echo",
|
|
34
|
+
"run",
|
|
35
|
+
"secho",
|
|
36
|
+
"should_color",
|
|
37
|
+
"style",
|
|
38
|
+
"unstyle",
|
|
39
|
+
"__version__",
|
|
40
|
+
]
|