Skip to content

Subcommand dispatch

Route a parsed command to a handler, optionally typed per command path. See the Subcommand dispatch guide.

Dispatch

Dispatch(doc: str)

Route a parsed command to a handler - the subcommand dispatch docopt itself omits.

Register one handler per command path with on. run then parses argv and calls the handler for the most specific command path that matched.

The handler receives the parsed Arguments, or an instance of schema bound from the result when the registration supplies one, so each subcommand gets its own typed view. An on() with no command path registers a fallback, used when no more specific path matches.

Example

app = Dispatch(doc); @app.on("user", "create") def create(args): ...; app.run().

Source code in src/docopt2/_core.py
def __init__(self, doc: str) -> None:
    self.doc: str = doc
    self._handlers: list[tuple[tuple[str, ...], _DispatchHandler, type[Any] | None]] = []

on

on(
    *command_path: str, schema: type[Any] | None = None
) -> Callable[[_HandlerT], _HandlerT]

Register the decorated handler for command_path (empty path = fallback handler).

Source code in src/docopt2/_core.py
def on(self, *command_path: str, schema: type[Any] | None = None) -> Callable[[_HandlerT], _HandlerT]:
    """Register the decorated handler for ``command_path`` (empty path = fallback handler)."""

    def register(handler: _HandlerT) -> _HandlerT:
        self._handlers.append((command_path, handler, schema))
        return handler

    return register

run

run(
    argv: list[str] | tuple[str, ...] | str | None = None,
    **options: Any,
) -> Any

Parse argv against the doc and invoke the handler for the matched command path.

Extra keyword arguments are forwarded to docopt (suggest, exit_code, ...). schema is not among them, since dispatch matches on the mapping and binds per handler.

Source code in src/docopt2/_core.py
def run(self, argv: list[str] | tuple[str, ...] | str | None = None, **options: Any) -> Any:
    """Parse ``argv`` against the doc and invoke the handler for the matched command path.

    Extra keyword arguments are forwarded to [`docopt`][docopt2.docopt] (``suggest``, ``exit_code``, ...).
    ``schema`` is not among them, since dispatch matches on the mapping and binds per handler.
    """
    if "schema" in options:
        # Forwarding it would hand docopt() a schema, and dispatch would then try to route on the
        # instance instead of the mapping - an AttributeError deep inside, for a legible mistake.
        raise TypeError("Dispatch.run() takes no `schema`; register one per handler with `on(..., schema=...)`")
    if options.get("complete", True) and self.doc is not None and COMPLETION_REQUEST_ENV in os.environ:
        # answered here: docopt() below is given no schema, and the handlers' ones are what complete values
        from docopt2._completion import reply_to_completion_request  # deferred, as in docopt()

        print(reply_to_completion_request(self.doc, table=self._table()))
        sys.exit()
    arguments = cast("Arguments", docopt(self.doc, argv, **options))
    # Thread the usage and the caller's exit_code onto any exit raised HERE, as docopt()'s own _exit
    # does - or str(exc) loses the Usage block and the process ignores the requested status.
    usage = single_usage_section(self.doc)  # the doc already parsed above, so this cannot raise
    exit_code = options.get("exit_code", 1)
    resolved = self._resolve(arguments)
    if resolved is None:
        summary = "no handler is registered for the given command"
        raise DocoptExit(diagnostic=Diagnostic(summary=summary), usage=usage, exit_code=exit_code)
    handler, schema = resolved
    if schema is None:
        return handler(arguments)
    try:
        bound = bind_schema(arguments, schema)
    except _CoercionError as exc:
        resolved_argv = sys.argv[1:] if argv is None else argv
        reading = (
            options.get("options_first", False),
            options.get("negative_numbers", False),
            options.get("allow_abbrev", True),
        )
        diagnostic = _coercion_diagnostic(self.doc, resolved_argv, exc, arguments, reading)
        raise DocoptExit(diagnostic=diagnostic, usage=usage, exit_code=exit_code) from exc
    return handler(bound)

check

check() -> list[Diagnostic]

Statically lint the usage message and the table, before any argv is parsed.

Returns what check returns for the usage message, and for every handler registered with a schema what check(doc, schema=...) returns, with one difference: the schema is held against the usage lines its handler can be run for, and not against the whole usage. So an element that only the lines of another command leave out is not reported as one that may be absent. Each such finding names its handler. A line is held whole: where one line serves two commands, (add <name> | list), <name> may be absent on it for the handler of add too.

A handler that no argv can reach is reported too: an "error" where its command path names what the usage does not have, a "warning" where another handler is the one run for every argv that gives its commands. A fallback, registered with no command path, is never reported.

Source code in src/docopt2/_core.py
def check(self) -> list[Diagnostic]:
    """Statically lint the usage message and the table, before any argv is parsed.

    Returns what [`check`][docopt2.check] returns for the usage message, and for every handler registered
    with a schema what ``check(doc, schema=...)`` returns, with one difference: the schema is held against
    the usage lines its handler can be run for, and not against the whole usage. So an element that only
    the lines of another command leave out is not reported as one that may be absent. Each such finding
    names its handler. A line is held whole: where one line serves two commands, ``(add <name> | list)``,
    ``<name>`` may be absent on it for the handler of ``add`` too.

    A handler that no argv can reach is reported too: an ``"error"`` where its command path names what the
    usage does not have, a ``"warning"`` where another handler is the one run for every argv that gives
    its commands. A fallback, registered with no command path, is never reported.
    """
    from docopt2._lint import check_table  # deferred: the linter is no part of a run

    return check_table(self.doc, self._table())