"""Program to generate documentation for a given PyToolConfig object.""" from __future__ import annotations from dataclasses import is_dataclass from typing import TYPE_CHECKING, Any, Generator if TYPE_CHECKING: from _typeshed import DataclassInstance from docutils.statemachine import StringList from sphinx.application import Sphinx from .pytoolconfig import PyToolConfig from .types import ConfigField from typing import get_origin from sphinx.ext.autodoc import ClassDocumenter from tabulate import tabulate from .fields import _gather_config_fields from .sources import Source from .universal_config import UniversalConfig def _type_to_str(type_to_print: type[Any]) -> str | None: if type_to_print is None: return None if get_origin(type_to_print) is None: try: return type_to_print.__name__ except AttributeError: return str(type_to_print) return str(type_to_print).replace("typing.", "") def _subtables(model: type[DataclassInstance]) -> dict[str, type[DataclassInstance]]: result = {} for name, field in _gather_config_fields(model).items(): if is_dataclass(field._type): result[name] = field._type return result def _generate_table( model: type[DataclassInstance], tablefmt: str = "rst", prefix: str = "", ) -> Generator[str, None, None]: header = ["name", "description", "type", "default"] model_fields: dict[str, ConfigField] = _gather_config_fields(model) command_line = any(field.command_line for field in model_fields.values()) if command_line: header.append("command line flag") table = [] for name, field in model_fields.items(): if not is_dataclass(field._type): row = [ f"{name}" if not prefix else f"{prefix}.{name}", field.description.replace("\n", " ") if field.description else None, _type_to_str(field._type), field._default, ] if field.universal_config: key = field.universal_config assert is_dataclass(UniversalConfig) universal_key = _gather_config_fields(UniversalConfig)[key.name] row[1] = universal_key.description row[3] = universal_key._default if command_line: cli_doc = field.command_line if cli_doc is not None: row.append(", ".join(cli_doc)) else: row.append(None) table.append(row) yield from tabulate(table, tablefmt=tablefmt, headers=header).split("\n") class PyToolConfigAutoDocumenter(ClassDocumenter): """Sphinx autodocumenter for pytoolconfig models.""" objtype = "pytoolconfigtable" content_indent = "" titles_allowed = True @classmethod def can_document_member( cls, member: Any, membername: str, # noqa: ARG003 isattr: bool, # noqa: ARG003 parent: Any, # noqa: ARG003 ) -> bool: """Check if member is dataclass.""" return is_dataclass(member) def add_directive_header(self, sig: str) -> None: """Remove directive headers.""" def add_content( self, more_content: StringList | None, # noqa: ARG002 no_docstring: bool = False, # noqa: ARG002 ) -> None: """Create simple table to document configuration options.""" source = self.get_sourcename() config = self.object for line in _generate_table(config): self.add_line(line, source) class PyToolConfigSourceDocumenter(ClassDocumenter): """Expiremental documenter for docmenting a source for pytoolconfig.""" objtype = "pytoolconfigsources" content_indent = "" titles_allowed = True @classmethod def can_document_member( cls, member: Any, membername: str, # noqa: ARG003 isattr: bool, # noqa: ARG003 parent: Any, # noqa: ARG003 ) -> bool: """Check if member is dataclass.""" return isinstance(member, Source) def add_directive_header(self, sig: str) -> None: """Remove directive headers.""" def add_content( self, more_content: StringList | None, # noqa: ARG002 no_docstring: bool = False, # noqa: ARG002 ) -> None: """Create simple table to document configuration options.""" source = self.get_sourcename() config = self.object for line in _generate_table(config): self.add_line(line, source) def setup(app: Sphinx) -> None: """Register automatic documenter.""" app.setup_extension("sphinx.ext.autodoc") app.add_autodocumenter(PyToolConfigAutoDocumenter) def _generate_documentation(config: PyToolConfig) -> Generator[str, None, None]: """Generate Markdown documentation for a given config model. This currently Beta at best. Do not use. """ yield "# Configuration\n" if len(config.sources) > 1: yield f"{config.tool} supports the following sources:\n" for idx, source in enumerate(config.sources): yield f" {idx}. {source.name}\n" else: name = config.sources[0].name yield f"{config.tool} supports the {name} format\n" yield "\n" for source in config.sources: if source.description: yield f"## {source.name} \n" yield source.description yield "\n" yield "## Options\n" yield from _generate_table(config.model, "github") yield "\n" for prefix, subtable in _subtables(config.model).items(): yield from _generate_table(subtable, "github", prefix) yield "\n" yield "\n"