Skip to content

Solganis/docopt2

Repository files navigation

docopt2
Typed successor to docopt. The usage message is the parser spec.
A drop-in replacement for docopt - a superset, not a rewrite.

CI Coverage PyPI version Python Downloads
zero runtime dependencies, pydantic support optional the typed API is checked by ty, mypy --strict, and pyright Documentation OpenSSF Scorecard


What is docopt2?

docopt2 is a command-line argument parser for Python. Most CLI libraries have you build the parser in code and generate a --help. docopt2 inverts that: you write the usage message, and that message is the parser.

Put the Usage: and Options: text in your docstring - the help you would write anyway - and docopt2 parses the command line against it. The help users read and the parser that runs are the same, and cannot drift.

The full documentation walks through Getting started and the guides.

Features

Typed results
Pass a schema, get a typed result back - fields coerced to their types, never a dict[str, Any].
Rustc-style diagnostics
Errors point at the mistake: colored carets in your argv and the usage, plus the closest usage line.
Subcommand dispatch
Route each subcommand to its own handler - no if args[...] ladder.
Shell completion
Tab-completion that knows your grammar: bash, zsh, fish, PowerShell, nushell.
Schema codegen
Never hand-write the schema - docopt2 stub generates it from your usage, in three styles.
Layered fallback
Resolve an option from [env: VAR] or [config: key] - CLI, then env, then config, then default.
Self-documenting --help
Opt into a colored, scoped help that shows where each value resolves from - env, config, default.
Example generation
Sample the argvs your usage accepts - for drift detection, fuzzing, and a Hypothesis strategy.
Static usage linter
Catch a broken usage before it ships - docopt2 check flags dead defaults and unusable options.
Usage formatter
Keep the Options: block aligned - docopt2 fmt reformats it, the format half to check's lint.
Round-trip codec
Turn a parsed result back into an argv that parses to it - format_argv, the inverse of docopt.
Compatibility checking
docopt2 compat old new reports the changes that would break callers - a breaking-change detector for your CLI.
pip install docopt2  # just change the import
"""Naval Fate.

Usage:
  naval ship new <name>...
  naval ship <name> move <x> <y> [--speed=<kn>]
  naval --help

Options:
  --speed=<kn>  Speed in knots [default: 10].
"""
from docopt2 import docopt

args = docopt(__doc__)
# args is a dict: {"ship": True, "<name>": ["titanic"], "move": True, "--speed": "10", ...}

docopt2 parses the same usage grammar, so switching over is a one-line import change and everything else is opt-in.

docopt2 reads the same usage DSL as docopt - the Usage: and Options: blocks are the spec.

SyntaxMeaning
commandA literal (sub)command, matched as-is.
<arg>, ARGA positional argument.
-o, --optionAn option (flag).
--option=<val>An option that takes a value.
[ ]Optional element(s).
( )Required group.
a | bMutually exclusive: choose one.
element...Repeatable: one or more.
[options]Stands in for every option listed under Options:.
--Ends option parsing. The rest is positional.
[default: <val>]An option's default value, declared under Options:.
[env: <var>]An option's environment-variable fallback.
[config: <key>]An option's config-file fallback (CLI > env > config > default).

The legend covers the essentials. Precedence and edge cases are in the full usage grammar.

docopt hands you a dict of strings - no autocomplete, no static types, coercion by hand at every call site:

args = docopt("Usage: app <host> <port> [--verbose]")
host = args["<host>"]        # unchecked, a bare string
port = int(args["<port>"])   # coerce by hand, at every call site

docopt2 takes a schema and gives you a typed result back:

@dataclasses.dataclass
class Args:
    host: str
    port: int                  # coerced from the parsed string
    verbose: bool

args = docopt("Usage: app <host> <port> [--verbose]", schema=Args)
args.port                      # statically an int, not a string

A dataclass, a TypedDict, the Cli base class, or a pydantic model all work as schema= -
and you don't hand-write it, docopt2 stub generates it from the usage.

Validated choices. A Literal or Enum field becomes a closed set - a bad value is rejected with the valid set listed and the closest match suggested.

When the arguments don't match, docopt reprints the usage and leaves you to find the mistake:

Usage:
  git commit [--message=<msg>] [--amend]
  git push [--force] <remote>

docopt2 points at it - in the argv and the usage that rejected it. The "did you mean" hint is opt-in: pass suggest=True.

A docopt2 error: 'unknown option --forcce' with a caret under the token in the argument vector and a second caret under --force in the usage, plus a 'did you mean --force?' hint

The same two carets flag a malformed usage at import time - a broken spec fails loudly, not silently - and a value that will not coerce to its typed field:

A docopt2 error: 'invalid value for --port' with a caret under abc in the argument vector and a second caret under --port=<n> in the usage, plus 'note: abc is not a valid int'

Closest usage line. docopt2 finds the usage line you got furthest into and carets the single element it still needs, rather than reprinting the whole usage. It needs a matched command or option to go on: an argv that matches no line at all falls back to the plain mismatch message.

Dispatch routes each command to its own handler - no if args["..."] ladder:

from docopt2 import Dispatch

app = Dispatch("""Usage:
  git add <path>...
  git commit --message=<msg>
""")

@app.on("add")
def add(args):
    print(f"adding {args['<path>']}")

@app.on("commit")
def commit(args):
    print(f"committing {args['--message']!r}")

app.run()   # parse argv, call the matched command's handler

bash zsh fish PowerShell nushell

Generate the completion script for your shell. Tab then narrows to exactly what is valid at the cursor -
commands and options, never positional values - straight from the usage:

$ naval <TAB>
--help  --speed  ship
$ naval ship <TAB>
--speed  new
$ naval ship titanic move 1 2 <TAB>
--speed

Don't hand-write the schema - docopt2 stub generates it from your usage (a module docstring, a text file, or stdin):

$ docopt2 stub naval.py
@dataclasses.dataclass
class Args:
    ship: bool
    new: bool
    name: list[str]
    move: bool
    x: str | None
    y: str | None
    speed: str
    help: bool

Add --style=typeddict or --style=cli for the other shapes.
Widen a field by hand (speed: int) and the coercion is automatic.

Declare an option's fallback sources in the usage with [env: VAR] and [config: key]. docopt2 resolves each -
the command-line argument first, then the environment, then a config mapping you pass, then the [default: ...]:

doc = "Usage: prog [--port=<n>]\n\nOptions:\n  --port=<n>  Port [default: 80] [env: APP_PORT] [config: server.port]."

docopt(doc, "", config={"server": {"port": 8080}})             # {'--port': '8080'} - config
os.environ["APP_PORT"] = "7000"
docopt(doc, "", config={"server": {"port": 8080}})             # {'--port': '7000'} - env wins
docopt(doc, "--port=9000", config={"server": {"port": 8080}})  # {'--port': '9000'} - CLI wins

docopt2 never reads the file - you load it however you like (TOML, JSON, a [tool.<prog>] table) and pass the mapping, so you are never tied to one format.

Know where a value came from. args.source(name) reports the layer that actually won - so you can log or branch on provenance instead of guessing:

docopt(doc, "--port=9000", config={"server": {"port": 8080}}).source("--port")   # Source.CLI

Scaffold the config file from your usage. docopt2 config-template writes a ready-to-fill TOML skeleton - every [config: key], seeded with its default:

$ docopt2 config-template app.py
[server]
port = 80  # --port, env APP_PORT

--help prints your usage verbatim by default. Opt into help_style="rich" for an aligned, colored screen that documents where each value resolves from, reading the [env, config, default] chain straight out of the usage you already wrote:

A docopt2 rich --help: a bold Usage, then Options with green option names, aligned descriptions, and a dimmed source chain per option - '[env: PORT, config: server.port, default: 8080]' - documenting the resolution order

Because the sources are declared right in the usage text, the help writes itself. It also scopes to the subcommand the user typed - git commit --help shows only commit.

docopt2 examples (or generate_examples) walks the usage grammar into concrete invocations it accepts -
derived from the spec, so they cannot drift from what docopt parses:

$ docopt2 examples naval.py --count=4 --seed=5
--help
ship v1 move v2 v3
ship new v4 v5
ship new v6

Golden-file them to catch grammar drift, fuzz your parser with the accepted set, or add --invalid for the reject-set.

Property-test with Hypothesis. The same sampler is a shrinking Hypothesis strategy - every draw is an argv your usage accepts.

docopt2 check (or docopt2.check(doc) in code) lints the usage grammar itself -
catching defects the parser would otherwise accept in silence:

A docopt2 check warning: option --verbose is declared but never used, with a caret under its declaration in the options section and a help line on how to fix it

It flags dead [default: ...] values, options declared but never usable,
ambiguous variadic positionals, and redundant alternatives.

docopt2 fmt (or format_usage) reformats the Options: block - aligning every description into one column,
normalizing each spec, stripping trailing space - the format half to check's lint. Take a drifted block:

Options:
  --port=<n>  Port [default: 8080].
  --host=<h>   Interface [default: 127.0.0.1].
  -v, --verbose  Be loud.

docopt2 fmt lines it up, without changing what the usage parses to and leaving the Usage: patterns untouched:

Options:
  --port=<n>    Port [default: 8080].
  --host=<h>    Interface [default: 127.0.0.1].
  -v --verbose  Be loud.

The change is layout-only and idempotent. docopt2 fmt prints to stdout - pipe it back to rewrite the file, or diff it in CI to flag a usage that is not formatted.

docopt parses an argv into a result. format_argv does the inverse - the same usage message drives both directions:

doc = "Usage: prog <src> <dst> [--force]"
args = docopt(doc, "a b --force")
format_argv(args, doc)   # ['a', 'b', '--force'] - which docopt parses straight back to args

It emits every element that carries a value: what you supplied, plus whatever [env:] or [config:] resolved.
Each candidate is verified by re-parsing, and an ambiguous grammar raises rather than return a wrong argv.

Your usage message is your interface, so check_compat (and docopt2 compat) reports the changes that would break callers - invocations the old usage accepts that the new one rejects:

check_compat("Usage: git push [--force] <remote>", "Usage: git push <remote> <branch>")
# ['option `--force` removed', '`push v1` no longer accepted']

It reports only definite breaks - never a "compatible" all-clear - so docopt2 compat before after drops into a release gate like a linter.

Read the full documentation · start with Getting started


MIT License · derived from docopt · see NOTICE

About

Typed successor to docopt: the usage message is the parser spec. Zero-dependency, drop-in superset.

Topics

Resources

License

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages