Diagnostics¶
When the arguments don't match, docopt2 does not just reprint the usage. It points at the offending token, in the argument vector and in the usage that rejected it:
A failed parse raises DocoptExit carrying that diagnostic. The block above
is exactly what str(exc) prints for the run below, minus the usage message that DocoptExit always
appends beneath it:
from docopt2 import docopt
doc = """Git push.
Usage:
git push [--force] <remote>
Options:
--force Force the push.
"""
docopt(doc, "push --forcce origin") # raises the diagnostic above
Read the two snippets top to bottom:
in the arguments:- your argv, joined with spaces, with a caret under--forcce, the token that had no place in the usage.in the usage:- the usage line that declares the option you probably meant, with a caret under--force. This is the cross-reference: it ties the mistake in argv to the exact spot in the spec that rejected it, so you are not left scanning the whole usage yourself.
The help: line carries the "did you mean" hint. See Hints for a typo below for when it
fires and how to turn it off.
One shape for every error¶
Every error the usage produces is lowered to the shape above, whether the user mistyped an argument or the developer wrote a malformed usage:
- a bold
error:heading with a one-line summary - captioned source snippets with carets
- optional
note:andhelp:lines
Those last two follow rustc's split and are not interchangeable. A note: gives context: why the input was
rejected, or why this usage line is the one shown. A help: proposes a concrete fix, such as a "did you
mean". A diagnostic with nothing to suggest carries no help: at all.
The static usage linter reuses the same grammar with a yellow warning: heading, so a warning
and a hard error read the same way.
A schema that does not fit its usage is a different animal
A field naming no element, or an annotation nothing can coerce to, is a programming error rather than a
usage error. It raises a plain DocoptLanguageError with no source to point at.
Both places, not only typos¶
The second snippet is not exclusive to typos.
Any time the offending argv token is an option the usage actually declares, the diagnostic carets both places. Two branches of a mutually exclusive group is the common case:
doc = "Usage: prog (--fast | --slow)\n\nOptions:\n --fast Fast.\n --slow Slow."
docopt(doc, "--fast --slow")
Note
str(exc) is always plain text, so a caught diagnostic inspects and logs cleanly. When an error goes
unhandled and the interpreter prints it, the copy sent to the terminal is colored - automatically when
stderr is a TTY, and never when the NO_COLOR environment variable is set.
Piped or redirected output stays plain, so color never leaks into a file or a captured test.
The usage line you were closest to¶
When the arguments fall short of a multi-line usage, most parsers - docopt included - meet the mismatch by reprinting the whole usage and leaving you to work out which line you meant and what is missing:
docopt2 does better: it finds the line you got furthest into and points a caret at the one element still missing. A matched command is strong evidence of intent, so the line whose command you typed wins even when a later positional or option is absent:
doc = """Usage:
git push [--force] <remote>
git commit --message=<msg>
git add <path>...
Options:
--force Force.
--message=<msg> Message."""
docopt(doc, "push")
The snippet shows only the closest line - git push, not commit or add - because the caret is placed
in that line. Typing git commit instead carets --message=<msg> on the second line. git deploy prod
on a deploy <env> <version> line carets the missing <version>.
The ranking fires only with real evidence. A leading word that matches no line's command (git clone) is
not treated as a near-miss - there is nothing to be "closest" to - so docopt2 falls back to the plain
mismatch message rather than guess a line at random. A single-line usage has no alternatives to rank, so
it reports its missing element directly.
A value that does not fit its type¶
A parse can succeed and still fail when a value cannot be coerced to its schema field
type. That raises DocoptExit too, rendered with the same two-span caret:
the offending value in the argv, tied to the usage element that declared its type.
import dataclasses
from docopt2 import docopt
@dataclasses.dataclass
class Args:
port: int
doc = "Usage: prog --port=<n>"
docopt(doc, "--port=abc", schema=Args) # raises the diagnostic below
The caret stands under the token that held the value, whatever else the line holds. With prog abc abc
against Usage: prog <name> <count> and count: int, it is under the second abc.
Of a repeated element the one value that is refused is named, by the type of one value:
@dataclasses.dataclass
class Tags:
tag: list[int]
docopt("Usage: prog [--tag=<n>]...", "--tag=7 --tag 7x --tag=9", schema=Tags) # raises the diagnostic below
A value that was never in the arguments is shown where it was written. Here the option is declared with
[env: APP_PORT], and the variable holds http:
A value from a [config: ...] key is shown the same way, as server.port = http. A [default: ...] that
its own field refuses is shown as [default: http]. Of a default of several values the line is rebuilt
from the values, one space between them.
Only a text has a place. A flag or a counted flag that the arguments gave and its type refuses, -vvv
against Literal[0, 1, 2], is shown by its usage element alone.
Only a user value gets this two-span treatment. A schema that disagrees with the usage - a field with
no matching element, an unsupported annotation - is the developer's error, not the user's, and raises
DocoptLanguageError with no argv caret. See
Typed results.
A malformed usage at import time¶
A usage message that cannot be parsed raises DocoptLanguageError with
the same caret treatment, so a broken spec fails loudly rather than silently. Because docopt2 parses the
usage on the first docopt() call, this is a developer error surfaced as soon as the interface is
exercised, not a user error.
An unclosed group points its single caret at the opener that was never closed:
Mismatched delimiters draw two carets in one snippet, the opener and the closer that cannot match it:
A closing bracket with nothing open is caught the same way, with the summary unexpected closing.
Tip
To catch these before any argv is ever parsed, run the usage linter. It renders the same diagnostics ahead of time, as warnings, so a malformed spec is reported in a test rather than on a user's first bad command.
Hints for a typo¶
A mistyped long option or a mistyped command gets a "did you mean" hint. The option case is the diagnostic at the top of this page. A command reads the same way:
The two hints fire on different evidence.
- An option is hinted when it resembles a known long option within an edit-distance threshold.
--foris an unambiguous prefix, so withallow_abbrev=True(the default) it de-abbreviates to--forceand is accepted, never flagged.--forcceis a prefix of nothing, and that is what marks it as a typo. - A command is hinted only when the correction is proven. The token must resemble a command of the usage,
and the usage must accept the argv once that command stands in its place. So
push comit extragets no command hint. Withcommitthere it would still not parse, andcomitmay well be the name of a remote.
Short flags get no hint. They fall through to the standard unexpected argument diagnostic.
Turning hints off¶
Pass suggest=False and the same typo is reported by the generic mismatch path:
doc = "Usage:\n git push [--force] <remote>\n\nOptions:\n --force Force.\n"
docopt(doc, "push --forcce origin", suggest=False)
See also¶
- Exceptions -
DocoptExitandDocoptLanguageError. - Usage linting to catch defects before any argv is parsed.
- Usage DSL - the grammar these diagnostics point back into.