Skip to main content

Typed successor to docopt: the usage message is the parser spec.

Project description

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

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


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 out of it; docopt2 goes the other way: you write the usage message, and that message is the parser.

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

The full documentation is on the website - it opens on a short introduction, then 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.
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 every argv 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.

Quick start

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", ...}

Every argument vector the original docopt accepts, docopt2 accepts identically -
so switching over is a one-line import change, and everything else is opt-in.

Usage syntax

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; the full usage grammar - precedence, edge cases, and how each form maps to the parsed result - lives on the site.

Why typed docopt?

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.

Diagnostics that point at the problem

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:

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. When several invocations are possible, docopt2 finds the one you got furthest into and carets the single element it still needs - not a generic "no match".

Subcommand dispatch

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

Shell completion

bash zsh fish PowerShell

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

Generate the schema from the usage

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.

Layered value resolution

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

Self-documenting --help

--help prints your usage verbatim by default. Opt into help_style="rich" for an aligned, colored screen that also
documents where each value resolves from - the [env, config, default] chain no other CLI library surfaces in its help:

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.

Sample the invocations your usage accepts

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.

Lint the usage before it ships

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.

Format the usage

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, so docopt2 fmt drops into a pre-commit hook or a format-check step in CI.

Round-trip: results back to argv

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 exactly what was provided and verifies each candidate by re-parsing, so docopt(format_argv(x)) == x always holds -
a round-trip for reproducible commands, safe subprocess argv, and property tests.

Compatibility checking

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

Project details


Download files

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

Source Distribution

docopt2-1.0.2.tar.gz (715.8 kB view details)

Uploaded Source

Built Distribution

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

docopt2-1.0.2-py3-none-any.whl (66.9 kB view details)

Uploaded Python 3

File details

Details for the file docopt2-1.0.2.tar.gz.

File metadata

  • Download URL: docopt2-1.0.2.tar.gz
  • Upload date:
  • Size: 715.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for docopt2-1.0.2.tar.gz
Algorithm Hash digest
SHA256 b39bdd72f24abe32d9de09185eb1f525b867e7ea52fcd2ff7b8f3639f8646bcf
MD5 1d1c5e714eba68195976610e1211ed04
BLAKE2b-256 fb32c67c8e6af518e4838d84ae7518e74a57af35c429b7ed764e1f7ad7a057c3

See more details on using hashes here.

Provenance

The following attestation bundles were made for docopt2-1.0.2.tar.gz:

Publisher: publish.yml on Solganis/docopt2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file docopt2-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: docopt2-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 66.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for docopt2-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f59befd13238ccf7b84ebf0b1034d3e6990582e77fc6005c18a97d925bda7a73
MD5 17889aa9a3e96a3428f2c819a750cda5
BLAKE2b-256 1ff29f9d17c17b271ecaa236c5816e0c6260601dccd24c6a54650f999f66eb2d

See more details on using hashes here.

Provenance

The following attestation bundles were made for docopt2-1.0.2-py3-none-any.whl:

Publisher: publish.yml on Solganis/docopt2

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page