Skip to content

Schema stubs

Generate a typed schema class from a usage message. See the Schema stubs guide.

generate_stub

generate_stub(
    doc: str,
    *,
    name: str = "Args",
    style: StubStyle = "dataclass",
) -> str

Generate a typed schema class from a usage message, to pass as docopt(doc, schema=...).

Parameters:

Name Type Description Default
doc str

The usage message (the same string given to docopt).

required
name str

Class name for the generated schema.

'Args'
style StubStyle

"dataclass" (default), "typeddict", or "cli" (a Cli subclass with the usage embedded, so Name.parse() works standalone).

'dataclass'

Returns:

Type Description
str

Python source for the schema class. Every field is typed from the grammar (str,

str

str | None, int, bool, list[str]). Narrow a field by hand (port: int) and

str

the value is coerced for free.

str

A key the typed API cannot represent - two usage names collapsing to one field

str

(<foo-bar> and --foo-bar), or a name that is not a valid identifier - becomes a

str

leading # note instead of a field. The output is always valid Python, but a class the

str

note names is not usable as a schema: docopt(doc, schema=...) rejects that usage

str

outright. Fix the usage, as the note says, and regenerate.

Raises:

Type Description
DocoptLanguageError

The usage message is malformed (the same error docopt raises).

ValueError

name is not a valid Python identifier.

Source code in src/docopt2/_stub.py
def generate_stub(doc: str, *, name: str = "Args", style: StubStyle = "dataclass") -> str:
    """Generate a typed schema class from a usage message, to pass as ``docopt(doc, schema=...)``.

    Args:
        doc: The usage message (the same string given to [`docopt`][docopt2.docopt]).
        name: Class name for the generated schema.
        style: ``"dataclass"`` (default), ``"typeddict"``, or ``"cli"`` (a [`Cli`][docopt2.Cli]
            subclass with the usage embedded, so ``Name.parse()`` works standalone).

    Returns:
        Python source for the schema class. Every field is typed from the grammar (``str``,
        ``str | None``, ``int``, ``bool``, ``list[str]``). Narrow a field by hand (``port: int``) and
        the value is coerced for free.

        A key the typed API cannot represent - two usage names collapsing to one field
        (``<foo-bar>`` and ``--foo-bar``), or a name that is not a valid identifier - becomes a
        leading ``# note`` instead of a field. The output is always valid Python, but a class the
        note names is **not** usable as a schema: ``docopt(doc, schema=...)`` rejects that usage
        outright. Fix the usage, as the note says, and regenerate.

    Raises:
        DocoptLanguageError: The usage message is malformed (the same error [`docopt`][docopt2.docopt] raises).
        ValueError: ``name`` is not a valid Python identifier.
    """
    if not name.isidentifier() or keyword.iskeyword(name):
        raise ValueError(f"class name {name!r} is not a valid Python identifier")
    key_types = _key_annotations(doc)
    keys_by_field: dict[str, list[str]] = {}
    for key in key_types:
        keys_by_field.setdefault(_key_to_field(key), []).append(key)

    notes: list[str] = []
    fields: list[tuple[str, str]] = []
    handled: set[str] = set()
    for key, annotation in key_types.items():
        field = _key_to_field(key)
        if field in handled:
            continue
        handled.add(field)
        colliding = keys_by_field[field]
        if len(colliding) > 1:
            joined = ", ".join(f"`{usage_key}`" for usage_key in colliding)
            notes.append(f"# note: usage keys {joined} all map to `{field}`; give them distinct names to type them")
        elif not field.isidentifier() or keyword.iskeyword(field):
            notes.append(f"# note: usage key `{key}` is not a valid field name; rename it in the usage to type it")
        else:
            fields.append((field, annotation))
    return _render(name, style, doc, fields, notes)