from __future__ import annotations import importlib import re from pathlib import Path from typing import Any, Optional import requests import tomlkit as toml from typer import Argument, Option from typing_extensions import Annotated 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 version greater than 3.9 will result in richer documentation.[/]" )