File size: 3,914 Bytes
53dbcc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""Generate documentation for Cyclopts applications."""

from pathlib import Path
from typing import Annotated

from cyclopts.cli import app
from cyclopts.docs.types import (
    FORMAT_ALIASES,
    DocFormat,
    normalize_format,
)
from cyclopts.group import Group
from cyclopts.loader import load_app_from_script
from cyclopts.parameter import Parameter
from cyclopts.utils import UNSET


def _format_group_validator(argument_collection):
    format_arg = argument_collection.get("--format")
    output_arg = argument_collection.get("--output")

    if format_arg.value is UNSET:
        if output_arg.value is UNSET:
            raise ValueError('"--format" must be specified when output path is not provided.')

        if not output_arg.value or not hasattr(output_arg.value, "suffix"):
            raise ValueError('"--output" must be a valid file path when format is not specified.')

        suffix = output_arg.value.suffix.lower()

        if not suffix:
            raise ValueError(
                "Output file must have an extension to infer format (e.g., .md, .html, .rst). "
                'Please specify "--format" explicitly or add an extension to the output file.'
            )

        # Strip the leading period from suffix to look up in FORMAT_ALIASES
        suffix_key = suffix.lstrip(".")

        if not suffix_key:
            raise ValueError(
                "Invalid file extension. Output file must have a valid extension after the period. "
                'Please specify "--format" explicitly.'
            )

        inferred_format = FORMAT_ALIASES.get(suffix_key)

        if inferred_format is None:
            supported_extensions = [f".{ext}" for ext in FORMAT_ALIASES.keys() if len(ext) <= 4]
            raise ValueError(
                f'Cannot infer format from output extension "{suffix}". '
                f"Supported extensions: {', '.join(sorted(set(supported_extensions)))}. "
                f'Please specify "--format" explicitly.'
            )

        format_arg.value = inferred_format


format_group = Group(validator=_format_group_validator)


@app.command(default_parameter=Parameter(negative=""))
def generate_docs(
    script: str,
    output: Annotated[Path | None, Parameter(alias="-o", group=format_group)] = None,
    *,
    format: Annotated[
        DocFormat | None,
        Parameter(alias="-f", group=format_group),
    ] = None,
    include_hidden: bool = False,
    heading_level: int = 1,
    usage_name: str | None = None,
):
    """Generate documentation for a Cyclopts application.

    Parameters
    ----------
    script : str
        Python script path, optionally with ``':app_object'`` notation to specify
        the App object. If not specified, will search for App objects in the
        script's global namespace.
    output : Optional[Path]
        Output file path. If not specified, prints to stdout.
    format : Optional[DocFormat]
        Output format for documentation. If not specified, inferred from output
        file extension.
    include_hidden : bool
        Include hidden commands in documentation.
    heading_level : int
        Starting heading level for markdown format.
    usage_name : Optional[str]
        Replace the app name shown in ``Usage:`` lines with this string. For
        example, ``"uv run cli"`` for an app whose runtime name is ``"cli"``.
        Headings and anchors are unaffected. Default is None.
    """
    if format is None:  # Handled by _format_group_validator
        raise ValueError("Must specify format.")
    format = normalize_format(format)
    app_obj, _ = load_app_from_script(script)
    docs_content = app_obj.generate_docs(
        output_format=format,
        include_hidden=include_hidden,
        heading_level=heading_level,
        usage_name=usage_name,
    )

    if output:
        output.write_text(docs_content)
    else:
        print(docs_content)