180 lines
5.6 KiB
Python
180 lines
5.6 KiB
Python
"""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"
|