Usage linting¶
A usage message can be wrong in ways docopt accepts in silence. Describe --verbose under Options: and
forget to put it on a usage line, and your help text advertises a flag the parser then rejects with
unexpected argument. Nothing complains until a user tries it.
check (and the docopt2 check CLI) lints the grammar itself, before any argv is parsed. It never raises
and never touches parsing: it reads the Usage:, Options: and Arguments: blocks, and reports what it
finds as a list of warnings.
So you can wire it into a test or a CI step, and fail the build on a grammar mistake instead of shipping it.
From code¶
check(doc) takes the help string and returns a list[Diagnostic], every entry at "warning"
level. An empty list means the grammar is clean.
With a schema, check(doc, schema=Args), the list also holds "error" entries for the ways the schema
disagrees with the usage. See Checking a schema.
from docopt2 import check
doc = "Usage: prog\n\nOptions:\n --verbose Extra logging."
warnings = check(doc)
len(warnings) # 1
warnings[0].level # 'warning'
print(warnings[0].render())
Each entry is the same kind of diagnostic the runtime uses for parse errors.
warning.render() returns the block as text, and warning.render(color=True) produces the ANSI color shown
in the blocks below.
The list is purely informational. check does not mutate doc, and does not change how docopt() later
parses it.
Note
A usage message that does not parse (an unbalanced group, say) is reported as a single "error": the
one docopt() raises for it at every run, with its caret. Nothing else is looked at in such a message.
What it flags¶
check applies seven rules. Each catches a construct the parser tolerates but that almost always means
the usage message says something you did not intend.
| Warning | Fires when |
|---|---|
| unusable option | an option in Options: appears in no usage line and no [options] shortcut is present to absorb it |
| dead default (option) | a [default: ...] sits on a value-taking option that every usage pattern requires, so it can never be applied |
| dead default (argument) | a [default: ...] in an Arguments: section sits on an always-required positional |
| ambiguous variadics | two ... positionals share one sequence, leaving the token split between them undefined |
| redundant alternative | two branches of one alternation group are identical, so one can never be reached |
empty [options] |
a usage line uses [options] but no options are described |
| repeated annotation | one option carries [default: ...], [env: ...] or [config: ...] more than once |
Every warning is a Diagnostic. The unusable-option case, rendered, looks like this - the caret points
at the exact token in your source:
The other rules render the same way. Each example below pairs the smallest usage that triggers the
rule with the exact warning check returns.
Unusable options¶
An option described in Options: but named on no usage line can never be set, unless a [options] shortcut
is there to accept it. Three ways to fix it:
- add the option to a usage line
- add
[options], which accepts every described option and suppresses the warning - delete the description
Dead defaults¶
A [default: ...] only applies when the element is absent, so if every usage pattern requires the element,
the default is unreachable.
It fires on options declared in Options::
doc = "Usage: prog --port=<n>\n\nOptions:\n --port=<n> Port [default: 80]."
print(check(doc)[0].render(color=True))
and, identically, on positionals documented in an Arguments: section:
doc = "Usage: prog <host>\n\nArguments:\n <host> Host name [default: localhost]."
print(check(doc)[0].render(color=True))
The fix is either to wrap the element in [ ... ] so it becomes optional (and the default can fill
in), or to drop the default.
Ambiguous variadics¶
Two variadic positionals in the same sequence have no defined boundary. Given prog a b c, docopt2 cannot
know where <a>... stops and <b>... starts.
This warning carries no caret, since the defect is the pair, not one token.
It counts each ... over a positional as one unit along a single path. So both of these are fine:
(<a> <b>)...is one repeat with a fixed interleaving, not two competing ones.- Two variadics in separate
|branches never meet on the same path.
Redundant alternatives¶
Two identical branches inside one alternation group mean one of them can never match. Usually it is a copy-paste slip or a typo in what was meant to be a distinct branch.
Empty [options]¶
A usage line uses the [options] shortcut, but there is no Options: section for it to expand, so it
matches nothing.
Repeated annotations¶
An option carries the same annotation twice. For [env: ...] and [config: ...] the first one is the one
read, so a second source silently never applies. It is usually a leftover from an edit.
doc = "Usage: prog [--port=<n>]\n\nOptions:\n --port=<n> Port [config: server.port] [config: port]."
print(check(doc)[0].render(color=True))
Checking a schema¶
A schema is a second description of the same command line, and the two can
disagree. Binding raises DocoptLanguageError for such a disagreement, and for some of them only on
the argv that reaches it: a field that is not optional binds until the day its option is left out.
check(doc, schema=Args) finds every disagreement before any argv exists:
import dataclasses
from docopt2 import check
@dataclasses.dataclass
class Args:
out: str
level: int
verbose: str
doc = "Usage: prog [--out=<f>] [--level=<n>]... [--verbose]"
for finding in check(doc, schema=Args):
print(finding.render())
These findings are at "error" level, after the warnings about the usage itself. There are four kinds:
| Finding | Fires when |
|---|---|
| no matching element | a field names no element of the usage |
| two elements, one field | --name and <name> both map to the field name |
| may be absent | an element can be left out, and its field is neither T \| None nor given a default |
| does not fit | the type cannot hold what the element yields: a list for an element that is not repeated, a bool for an option that takes a value, a str for a flag |
Two more are about the schema as a whole: annotations that do not resolve, and a plain class with no constructor to take its fields.
A flag yields True, and False unless the usage always requires it. A type that takes neither is
reported, and so is one that is handed a value of another type. A type that takes one of the two is a
limit the author set, and is not reported.
A counted flag typed with a closed set, Literal[0, 1, 2] on [-v]..., is judged by its shape alone. The
set is the author's limit on the count: a fourth -v is refused at run time, as a value of the user's.
Which counts a usage can reach is not worked out, so Literal[1] on -v -v is not found.
Fields are held against elements, and no more than that. A default is not held against its own
annotation, which is a type checker's to judge. The schema is not constructed, so what its own __init__
or __post_init__ takes or refuses is not looked at. To judge a type the check calls the same coercion
that binding uses, so an Enum of the schema is called with the values that are tried.
The schema is held against the whole usage, as docopt(doc, schema=...) binds it. A handler of a
Dispatch table binds its schema only on the lines it is run for, and
Dispatch.check() holds each schema of a table against those lines.
The check covers a dataclass, a TypedDict and a Cli subclass. A pydantic model binds by rules of its
own, so it gets a single "warning" that says it was not checked. So does a schema whose usage message
does not parse.
From the command line¶
docopt2 check <source> runs the same lint over a usage message read from <source>, which is one of:
- a
.pyfile - its module docstring is read (without importing the module), - a text file of raw usage,
-for standard input.
Warnings are written to standard error. The command exits 0 when the grammar is clean and 1 when
anything was reported, a usage message that does not parse included, so it drops into a pre-commit hook or
a CI job as a gate:
The cli.py above is the module whose docstring is:
Usage: greet --name=<who>
Options:
--name=<who> Who to greet [default: world].
--shout Upper-case the greeting.
Tip
Because a warning sets exit status 1, docopt2 check src/app.py in CI fails the build on a
grammar defect - the same guarantee the check function gives a test,
without importing your program.
See also¶
checkin the API reference.- Diagnostics for the runtime (mismatch) counterpart.
- Usage DSL - the grammar
checklints. - Generate a stub - the other
docopt2subcommand.