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