Skip to main content

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_alllist-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() or manifest() — they're invisible defaults
  • Yield to named subcommands (dispatch checks registry first)
  • Delegate --help to 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fncli-0.1.4.tar.gz (33.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fncli-0.1.4-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file fncli-0.1.4.tar.gz.

File metadata

  • Download URL: fncli-0.1.4.tar.gz
  • Upload date:
  • Size: 33.9 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for fncli-0.1.4.tar.gz
Algorithm Hash digest
SHA256 58794da76ca958d297129a32bea71a82affbd08d6f70d83a73a99f77a138767f
MD5 e3b5ed46ea2cbdd2b1c1dbac7b05bfa8
BLAKE2b-256 c4663591809b4228ba2327292db1b2dbb42bcef05e00d584983de06ffeba94de

See more details on using hashes here.

File details

Details for the file fncli-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: fncli-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for fncli-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d36d76457cc0a4c94a51c710b3e365d547cbacef31738cc84a967e8a29740afd
MD5 bfd70054e28887ce9293928afb7ae063
BLAKE2b-256 077f6a4c9544c01415fb3c68128e56fcaa20c62df6733789e2ab3ae88f69f31c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.4 This release

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.1

2 files

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