fncli
One decorator. Function signature is the CLI spec.
from fncli import cli
@cli("myapp")
def deploy(target: str, force: bool = False):
"""ship it"""
print(f"deploying to {target}")
$ myapp deploy prod --force
deploying to prod
Signature → parser. Docstring → help. Types → validation. One file, no dependencies.
Install
pip install fncli
Type mapping
| annotation | CLI behavior |
|---|---|
name: str |
required positional |
n: int |
required positional, coerced to int |
verbose: bool = False |
--verbose flag |
count: int = 10 |
--count 10 optional |
tags: list[str] |
positional varargs |
tags: list[str] = [] |
--tags a b c optional |
filter: str | None = None |
--filter optional |
Naming: dry_run → --dry-run. list_all → list-all. Trailing _ stripped: type_ → --type.
Subcommands
First argument to @cli() is the parent namespace.
@cli()
def version(): ... # → "version"
@cli("myapp")
def status(): ... # → "myapp status"
@cli("myapp server")
def start(port: int = 8080): ... # → "myapp server start"
Dispatch is longest-match. myapp server --help auto-lists subcommands.
Bare namespaces
When a namespace has one obvious default action, use bare=True. The namespace itself becomes the command — no subcommand needed.
@cli("ledger", bare=True, flags={"domain": ["-d"]}, required=["domain"])
def insight(content: str, domain: str | None = None):
"""log an insight"""
...
@cli("ledger insight")
def close(ref: str):
"""close an insight"""
...
$ ledger insight "auth is fragile" -d security # bare — namespace IS the verb
$ ledger insight close i/abc123 # named subcommand
$ ledger insight --help
usage: ledger insight <content> [-d DOMAIN]
log an insight
or: ledger insight <command> [args]
commands:
close close an insight
How it works: @cli("ledger", bare=True) on function insight registers a bare handler for namespace "ledger insight" (parent + fn name). Bare handlers:
- Accept full positional and flag arguments (same parsing as
@cli) - Don't appear in
commands()ormanifest()— they're invisible defaults - Yield to named subcommands (dispatch checks registry first)
- Delegate
--helpto namespace help (shows subcommands + bare usage)
When to use bare vs named: If the action is a lifecycle verb agents should know (propose, commit, approve), name it. If the action is just "the thing this namespace does" (create/add), make it bare.
Stacking bare + named for commands that are both the default AND a lifecycle verb:
@cli("ledger", name="decision", bare=True, required=["why"])
@cli("ledger decision", required=["why"])
def propose(content: str, why: str | None = None):
"""propose a decision"""
...
Both ledger decision "X" --why "Y" and ledger decision propose "X" --why "Y" work. Keep decorator params in sync — they're adjacent so drift is visible.
@cli() options
@cli(
"myapp", # parent namespace
name="st", # override command name (default: fn.__name__)
description="...", # override help text (default: fn.__doc__)
flags={...}, # custom flag names or positional-optional
help={...}, # per-param help: {"param": "description"}
required=["param"], # force a defaulted param to be required
aliases=["s"], # additional dispatch keys
default=True, # run when parent is invoked with no subcommand match
bare=True, # register as bare handler for parent + fn_name namespace
readonly=True, # metadata tag; query with readonly()
meta={...}, # arbitrary metadata; query with meta() / where()
)
Flags
@cli("myapp", flags={"output": ["-o", "--output"], "target": []})
def build(target: str | None = None, output: str = "dist"): ...
["-o", "--output"]— custom short + long flags[]— positional-optional: consumed by position
Aliases
@cli("myapp", aliases=["s", "stat"])
def status(): ...
# myapp s, myapp stat, myapp status all work
fncli.alias("myapp server tail", "myapp tail")
fncli.alias_namespace("myapp log", "myapp add")
Error handling
from fncli import UsageError, StateError
@cli("myapp")
def deploy(env: str):
if env not in ("staging", "prod"):
raise UsageError(f"unknown env: {env}") # stderr + exit 1 + "Run --help"
UsageError → appends "Run --help for usage." StateError → does not (correct syntax, wrong state).
Return an int for exit code. None / no return → exit 0.
Entrypoint
fncli.run(["myapp", *sys.argv[1:]]) # dispatch + sys.exit
fncli.dispatch(argv) # returns exit code
fncli.try_dispatch(argv) # returns None on miss
autodiscover scans for @cli( and auto-imports — no routing table:
fncli.autodiscover(Path(__file__).parent, "myapp")
fncli.run(["myapp", *sys.argv[1:]])
Unknown commands get fuzzy suggestions:
$ myapp strat
Unknown command: strat. Did you mean: start, status?
Testing
result = fncli.invoke(["myapp", "deploy", "prod"])
assert result.exit_code == 0
assert "deploying to prod" in result.stdout
invoke() captures stdout, stderr, and exit code. Traps SystemExit.
Shell completions
eval "$(myapp completions zsh)" # bash, zsh, fish
Selftest
$ myapp selftest # smoke-test --help on all commands
$ myapp selftest --live # also run readonly no-arg commands
Introspection
fncli.commands() # sorted list of all registered keys
fncli.entries() # [(key, fn, params), ...]
fncli.manifest() # structured dict — for agent consumption
fncli.meta(key) # metadata dict
fncli.where(**kwargs) # keys matching metadata predicates
fncli.readonly(key) # True if readonly=True
Metadata
Release files for fncli 0.1.4
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 | |
|---|---|---|---|---|
| fncli-0.1.4.tar.gz | 33.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fncli-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.8 kB
Release files / fncli-0.1.4.tar.gz
| Download URL | fncli-0.1.4.tar.gz |
|---|---|
| Size | 33.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
58794da76ca958d297129a32bea71a82affbd08d6f70d83a73a99f77a138767f
|
|
BLAKE2b-256 checksum How to use checksums |
c4663591809b4228ba2327292db1b2dbb42bcef05e00d584983de06ffeba94de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / fncli-0.1.4-py3-none-any.whl
| Download URL | fncli-0.1.4-py3-none-any.whl |
|---|---|
| Size | 13.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d36d76457cc0a4c94a51c710b3e365d547cbacef31738cc84a967e8a29740afd
|
|
BLAKE2b-256 checksum How to use checksums |
077f6a4c9544c01415fb3c68128e56fcaa20c62df6733789e2ab3ae88f69f31c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.2 {"installer":{"name":"uv","version":"0.12.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for fncli 0.1.4
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.