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 = doc
    self._handlers: list[tuple[tuple[str, ...], _DispatchHandler, type[Any] | None]] = []

on

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

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[[_DispatchHandler], _DispatchHandler]:
    """Register the decorated handler for ``command_path`` (empty path = fallback handler)."""

    def register(handler: _DispatchHandler) -> _DispatchHandler:
        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=...)`")
    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
        diagnostic = _coercion_diagnostic(self.doc, resolved_argv, exc)
        raise DocoptExit(diagnostic=diagnostic, usage=usage, exit_code=exit_code) from exc
    return handler(bound)