Source code for docs_cli.cli

"""docs command-line interface (Typer).

Builds and previews the asd-def Sphinx site, a thin wrapper over sphinx-build and
sphinx-autobuild, so the documentation has a single entry point.
"""

from __future__ import annotations

import shutil
import subprocess
import sys
from pathlib import Path

import typer

app = typer.Typer(
    name="docs",
    help="Build, version, and live-preview the asd-def Sphinx documentation.",
    no_args_is_help=True,
    add_completion=False,
)


# ---- helpers -----------------------------------------------------------------
def _docs_root() -> Path:
    """Walk up from the CWD to the nearest ancestor holding docs/source/conf.py."""
    for d in (Path.cwd(), *Path.cwd().parents):
        if (d / "docs" / "source" / "conf.py").is_file():
            return d
    typer.echo("no docs/source/conf.py at or above the current directory", err=True)
    raise typer.Exit(1)


def _tool(name: str) -> str:
    """Resolve a console script (sphinx-build, sphinx-autobuild) from the environment."""
    found = shutil.which(name) or str(Path(sys.executable).parent / name)
    if not Path(found).exists():
        typer.echo(f"{name!r} not found; run `uv sync --group docs`", err=True)
        raise typer.Exit(1)
    return found


def _run(cmd: list[str]) -> None:
    """Run a docs subprocess, exiting non-zero if it fails."""
    try:
        code = subprocess.call(cmd)
    except KeyboardInterrupt:
        code = 130
    if code:
        raise typer.Exit(code)


[docs] @app.command() def build( builder: str = typer.Option("html", "--builder", "-b", help="Sphinx builder."), strict: bool = typer.Option(False, "--strict", "-W", help="Turn warnings into errors."), ) -> None: """Build the docs once into docs/build/<builder> (HTML by default).""" root = _docs_root() cmd = [_tool("sphinx-build"), "-b", builder] if strict: cmd += ["-W", "--keep-going"] cmd += [str(root / "docs" / "source"), str(root / "docs" / "build" / builder)] _run(cmd)
[docs] @app.command() def versions( local: bool = typer.Option( False, "--local", "-l", help="Build only the working tree with mock version data." ), ) -> None: """Build every released version into docs/build, driven by docs/poly.py.""" root = _docs_root() cmd = [_tool("sphinx-polyversion"), str(root / "docs" / "poly.py")] if local: cmd.append("--local") _run(cmd)
[docs] @app.command() def serve( host: str = typer.Option("127.0.0.1", "--host", help="Bind address."), port: int = typer.Option(8000, "--port", "-p", help="Bind port."), ) -> None: """Serve the docs with live reload, rebuilding on edits to docs/ and packages/.""" root = _docs_root() cmd = [ _tool("sphinx-autobuild"), str(root / "docs" / "source"), str(root / "docs" / "build" / "html"), "--watch", str(root / "packages"), "--host", host, "--port", str(port), "--open-browser", ] _run(cmd)
[docs] @app.command() def clean() -> None: """Remove the docs/build directory.""" shutil.rmtree(_docs_root() / "docs" / "build", ignore_errors=True)
if __name__ == "__main__": app()