from __future__ import annotations import importlib import re from pathlib import Path from typing import Annotated, Any, Optional import requests import tomlkit as toml from typer import Argument, Option from gradio.analytics import custom_component_analytics from gradio.cli.commands.display import LivePanelDisplay from ._docs_assets import css from ._docs_utils import extract_docstrings, get_deep, make_markdown, make_space def _docs( path: Annotated[ Path, Argument(help="The directory of the custom component.") ] = Path("."), demo_dir: Annotated[ Optional[Path], Option(help="Path to the demo directory.") ] = None, demo_name: Annotated[Optional[str], Option(help="Name of the demo file.")] = None, readme_path: Annotated[ Optional[Path], Option(help="Path to the README.md file.") ] = None, space_url: Annotated[ Optional[str], Option(help="URL of the Space to use for the demo.") ] = None, generate_space: Annotated[ bool, Option( help="Create a documentation space for the custom compone.", is_flag=True ), ] = True, generate_readme: Annotated[ bool, Option(help="Create a README.md file for the custom component.", is_flag=True), ] = True, suppress_demo_check: Annotated[ bool, Option( help="Suppress demo warnings and errors.", is_flag=True, ), ] = False, ): """Runs the documentation generator.""" custom_component_analytics( "docs", None, None, None, None, ) _component_dir = Path(path).resolve() _demo_dir = Path(demo_dir).resolve() if demo_dir else Path("demo").resolve() _demo_name = demo_name if demo_name else "app.py" _demo_path = _demo_dir / _demo_name _readme_path = ( Path(readme_path).resolve() if readme_path else _component_dir / "README.md" ) if not generate_space and not generate_readme: raise ValueError("Must generate at least one of space or readme") with LivePanelDisplay() as live: live.update( f":page_facing_up: Generating documentation for [orange3]{str(_component_dir.name)}[/]", add_sleep=0.2, ) live.update( f":eyes: Reading project metadata from [orange3]{_component_dir}/pyproject.toml[/]\n" ) if not (_component_dir / "pyproject.toml").exists(): raise ValueError( f"Cannot find pyproject.toml file in [orange3]{_component_dir}[/]" ) with open(_component_dir / "pyproject.toml", encoding="utf-8") as f: data = toml.loads(f.read()) name = get_deep(data, ["project", "name"]) if not isinstance(name, str): raise ValueError("Name not found in pyproject.toml") run_command( live=live, name=name, suppress_demo_check=suppress_demo_check, pyproject_toml=data, generate_space=generate_space, generate_readme=generate_readme, type_mode="simple", _demo_path=_demo_path, _demo_dir=_demo_dir, _readme_path=_readme_path, space_url=space_url, _component_dir=_component_dir, ) def run_command( live: LivePanelDisplay, name: str, pyproject_toml: dict[str, Any], suppress_demo_check: bool, generate_space: bool, generate_readme: bool, type_mode: str, _demo_path: Path, _demo_dir: Path, _readme_path: Path, space_url: str | None, _component_dir: Path, simple: bool = False, ): with open(_demo_path, encoding="utf-8") as f: demo = f.read() pypi_exists = requests.get(f"https://pypi.org/pypi/{name}/json").status_code pypi_exists = pypi_exists == 200 or False local_version = get_deep(pyproject_toml, ["project", "version"]) description = str(get_deep(pyproject_toml, ["project", "description"]) or "") repo = get_deep(pyproject_toml, ["project", "urls", "repository"]) space = ( space_url if space_url else get_deep(pyproject_toml, ["project", "urls", "space"]) ) if not local_version and not pypi_exists: raise ValueError( f"Cannot find version in pyproject.toml or on PyPI for [orange3]{name}[/].\nIf you have just published to PyPI, please wait a few minutes and try again." ) module = importlib.import_module(name) (docs, type_mode) = extract_docstrings(module) if generate_space: if not simple: live.update(":computer: [blue]Generating space.[/]") source = make_space( docs=docs, name=name, description=description, local_version=local_version if local_version is None else str(local_version), demo=demo, space=space if space is None else str(space), repo=repo if repo is None else str(repo), pypi_exists=pypi_exists, suppress_demo_check=suppress_demo_check, ) with open(_demo_dir / "space.py", "w", encoding="utf-8") as f: f.write(source) if not simple: live.update( f":white_check_mark: Space created in [orange3]{_demo_dir}/space.py[/]\n" ) with open(_demo_dir / "css.css", "w", encoding="utf-8") as f: f.write(css) if generate_readme: if not simple: live.update(":pencil: [blue]Generating README.[/]") readme = make_markdown( docs, name, description, local_version, demo, space, repo, pypi_exists ) readme_content = Path(_readme_path).read_text() with open(_readme_path, "w", encoding="utf-8") as f: yaml_regex = re.search( "(?:^|[\r\n])---[\n\r]+([\\S\\s]*?)[\n\r]+---([\n\r]|$)", readme_content ) if yaml_regex is not None: readme = readme_content[: yaml_regex.span()[-1]] + readme f.write(readme) if not simple: live.update( f":white_check_mark: README generated in [orange3]{_readme_path}[/]" ) if simple: short_readme_path = Path(_readme_path).relative_to(_component_dir) short_demo_path = Path(_demo_dir / "space.py").relative_to(_component_dir) live.update( f":white_check_mark: Documentation generated in [orange3]{short_demo_path}[/] and [orange3]{short_readme_path}[/]. Pass --no-generate-docs to disable auto documentation." ) if type_mode == "simple": live.update( "\n:orange_circle: [red]The docs were generated in simple mode. Updating python to a more recent version will result in richer documentation.[/]" )