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")   # 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: 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

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
error: invalid value for `--tag` | | in the arguments: | --tag=7 --tag 7x --tag=9 | ^^ expected int | | in the usage: | Usage: prog [--tag=<n>]... | ^^^^^^^^^ typed as list[int] | = note: `7x` is not a valid int

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:

$ APP_PORT=http prog error: invalid value for `--port` | | in the environment: | APP_PORT=http | ^^^^ expected int | | in the usage: | Usage: prog [--port=<n>] | ^^^^^^^^^^ typed as int | = note: `http` is not a valid int

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:

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.

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:

doc = "Usage:\n  git commit [--amend]\n  git push [--force] <remote>"
docopt(doc, "comit --amend")
error: unknown command `comit` | | in the arguments: | comit --amend | ^^^^^ not a known command | | in the usage: | git commit [--amend] | ^^^^^^ `commit` is defined here | = help: did you mean `commit`?

The two hints fire on different evidence.

  • An option is hinted when it resembles a known long option within an edit-distance threshold. --for is an unambiguous prefix, so with allow_abbrev=True (the default) it de-abbreviates to --force and is accepted, never flagged. --forcce is 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 extra gets no command hint. With commit there it would still not parse, and comit may 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)
error: unexpected argument `--forcce` | | in the arguments: | push --forcce origin | ^^^^^^^^ not allowed here |

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.