Skip to content

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:

error: unknown option `--forcce` | | in the arguments: | push --forcce origin | ^^^^^^^^ not a known option | | in the usage: | git push [--force] <remote> | ^^^^^^^ `--force` is defined here | = help: did you mean `--force`?

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", suggest=True)   # 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. It appears only with suggest=True. See Opting into hints below.

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: and help: 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")
error: unexpected argument `--slow` | | in the arguments: | --fast --slow | ^^^^^^ not allowed here | | in the usage: | Usage: prog (--fast | --slow) | ^^^^^^ declared here | = help: give it at most once, not with a mutually exclusive option

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:

Usage:
  git push [--force] <remote>
  git commit --message=<msg>
  git add <path>...

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")
error: missing required `<remote>` | | in the usage: | git push [--force] <remote> | ^^^^^^^^ required here | = note: of 3 usage patterns, your arguments came closest to this one

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
error: invalid value for `--port` | | in the arguments: | --port=abc | ^^^ expected int | | in the usage: | Usage: prog --port=<n> | ^^^^^^^^^^ typed as int | = note: `abc` is not a valid int

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:

docopt("Usage: prog [--force <remote>", "push")
error: unclosed `[` | | in the usage: | Usage: prog [--force <remote> | ^ opened here, but never closed | = help: add `]` to close the group

Mismatched delimiters draw two carets in one snippet, the opener and the closer that cannot match it:

docopt("Usage: prog (a | b]", "a")
error: mismatched delimiters: `(` is closed by `]` | | in the usage: | Usage: prog (a | b] | ^ `(` opens the group here | ^ `]` cannot close it | = help: close it with `)`, or open with `[`

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.

Opting into hints

The "did you mean" hint is opt-in. Without it, 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 defaults to False
error: unexpected argument `--forcce` | | in the arguments: | push --forcce origin | ^^^^^^^^ not allowed here |

Pass suggest=True and docopt2 upgrades the message to the unknown option form shown at the top of this page: the "did you mean --force?" help line, plus the usage cross-reference. That fires when a mistyped long option resembles a known one within an edit-distance threshold.

The hint fires only on a genuine typo:

  • --for is an unambiguous prefix, so with allow_abbrev=True (the default) it already de-abbreviates to --force. It is accepted, never flagged.
  • --forcce is a prefix of nothing. That is what marks it as a typo worth a suggestion.

Note

suggest only affects long options (--name). Short flags and positional arguments fall through to the standard unexpected argument and missing or mismatched arguments diagnostics.

See also

  • Exceptions - DocoptExit and DocoptLanguageError.
  • Usage linting to catch defects before any argv is parsed.
  • Usage DSL - the grammar these diagnostics point back into.