Source code for autodoc2.render.rst_

"""Renderer for reStructuredText."""
from __future__ import annotations

import re
from textwrap import indent
import typing as t

from autodoc2.render.base import RendererBase

if t.TYPE_CHECKING:
    from autodoc2.utils import ItemData


_RE_DELIMS = re.compile(r"(\s*[\[\]\(\),]\s*)")


[docs]class RstRenderer(RendererBase): """Render the documentation as reStructuredText.""" EXTENSION = ".rst"
[docs] def render_item(self, full_name: str) -> t.Iterable[str]: item = self.get_item(full_name) if item is None: raise ValueError(f"Item {full_name} does not exist") type_ = item["type"] if type_ == "package": yield from self.render_package(item) elif type_ == "module": yield from self.render_module(item) elif type_ == "function": yield from self.render_function(item) elif type_ == "class": yield from self.render_class(item) elif type_ == "exception": yield from self.render_exception(item) elif type_ == "property": yield from self.render_property(item) elif type_ == "method": yield from self.render_method(item) elif type_ == "attribute": yield from self.render_attribute(item) elif type_ == "data": yield from self.render_data(item) else: self.warn(f"Unknown item type {type_!r} for {full_name!r}")
[docs] def generate_summary( self, objects: list[ItemData], alias: dict[str, str] | None = None ) -> t.Iterable[str]: alias = alias or {} yield ".. list-table::" yield " :class: autosummary longtable" yield " :align: left" yield "" for item in objects: full_name = item["full_name"] # TODO get signature (for functions, etc), plus sphinx also runs rst.escape if full_name in alias: yield f" * - :py:obj:`{alias[full_name]} <{full_name}>`" else: yield f" * - :py:obj:`{full_name}`" if self.show_docstring(item): yield f" - .. autodoc2-docstring:: {full_name}" if parser_name := self.get_doc_parser(full_name): yield f" :parser: {parser_name}" yield " :summary:" else: yield " -"
[docs] def render_package(self, item: ItemData) -> t.Iterable[str]: """Create the content for a package.""" if self.standalone and self.is_hidden(item): yield from [":orphan:", ""] full_name = item["full_name"] line = f":py:mod:`{full_name}`" yield from [line, "=" * len(line), ""] yield f".. py:module:: {full_name}" if self.no_index(item): yield " :noindex:" if self.is_module_deprecated(item): yield " :deprecated:" yield "" if self.show_docstring(item): yield f".. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield " :allowtitles:" yield "" visible_subpackages = [ i["full_name"] for i in self.get_children(item, {"package"}) ] if visible_subpackages: yield from [ "Subpackages", "-----------", "", ".. toctree::", " :titlesonly:", " :maxdepth: 3", "", ] for name in visible_subpackages: yield f" {name}" yield "" visible_submodules = [ i["full_name"] for i in self.get_children(item, {"module"}) ] if visible_submodules: yield from [ "Submodules", "----------", "", ".. toctree::", " :titlesonly:", " :maxdepth: 1", "", ] for name in visible_submodules: yield f" {name}" yield "" visible_children = [ i["full_name"] for i in self.get_children(item) if i["type"] not in ("package", "module") ] if not visible_children: return title = f"{item['type'].capitalize()} Contents" yield from [title, "-" * len(title), ""] if self.show_module_summary(item): for heading, types in [ ("Classes", {"class"}), ("Functions", {"function"}), ("Data", {"data"}), ("External", {"external"}), ]: visible_items = list(self.get_children(item, types)) if visible_items: yield from [ heading, "~" * len(heading), "", ] yield from self.generate_summary( visible_items, alias={ i["full_name"]: i["full_name"].split(".")[-1] for i in visible_items }, ) yield "" yield from ["API", "~~~", ""] for name in visible_children: yield from self.render_item(name)
[docs] def render_module(self, item: ItemData) -> t.Iterable[str]: """Create the content for a module.""" yield from self.render_package(item)
[docs] def render_function(self, item: ItemData) -> t.Iterable[str]: """Create the content for a function.""" short_name = item["full_name"].split(".")[-1] show_annotations = self.show_annotations(item) sig = f"{short_name}({self.format_args(item['args'], show_annotations)})" if show_annotations and item.get("return_annotation"): sig += f" -> {self.format_annotation(item['return_annotation'])}" yield f".. py:function:: {sig}" yield f" :canonical: {item['full_name']}" if self.no_index(item): yield " :noindex:" # TODO overloads if "async" in item.get("properties", []): yield " :async:" # TODO it would also be good to highlight if singledispatch decorated, # or, more broadly speaking, decorated at all yield "" if self.show_docstring(item): yield f" .. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield ""
[docs] def render_exception(self, item: ItemData) -> t.Iterable[str]: """Create the content for an exception.""" yield from self.render_class(item)
[docs] def render_class(self, item: ItemData) -> t.Iterable[str]: """Create the content for a class.""" short_name = item["full_name"].split(".")[-1] constructor = self.get_item(f"{item['full_name']}.__init__") sig = short_name if constructor and "args" in constructor: args = self.format_args( constructor["args"], self.show_annotations(item), ignore_self="self" ) sig += f"({args})" yield f".. py:{item['type']}:: {sig}" yield f" :canonical: {item['full_name']}" if self.no_index(item): yield " :noindex:" yield "" # TODO overloads if item.get("bases") and self.show_class_inheritance(item): yield " Bases: " + ", ".join( [self._reformat_cls_base_rst(b) for b in item.get("bases", [])] ) yield "" # TODO inheritance diagram if self.show_docstring(item): yield f" .. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield "" if self.config.class_docstring == "merge": init_item = self.get_item(f"{item['full_name']}.__init__") if init_item: yield " .. rubric:: Initialization" yield "" yield f" .. autodoc2-docstring:: {init_item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield "" for child in self.get_children( item, {"class", "property", "attribute", "method"} ): if ( child["full_name"].endswith(".__init__") and self.config.class_docstring == "merge" ): continue for line in self.render_item(child["full_name"]): yield indent(line, " ")
[docs] def render_property(self, item: ItemData) -> t.Iterable[str]: """Create the content for a property.""" short_name = item["full_name"].split(".")[-1] yield f".. py:property:: {short_name}" yield f" :canonical: {item['full_name']}" if self.no_index(item): yield " :noindex:" for prop in ("abstractmethod", "classmethod"): if prop in item.get("properties", []): yield f" :{prop}:" if item.get("return_annotation"): yield f" :type: {self.format_annotation(item['return_annotation'])}" yield "" if self.show_docstring(item): yield f" .. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield ""
[docs] def render_method(self, item: ItemData) -> t.Iterable[str]: """Create the content for a method.""" short_name = item["full_name"].split(".")[-1] show_annotations = self.show_annotations(item) sig = f"{short_name}({self.format_args(item['args'], show_annotations, ignore_self='self')})" if show_annotations and item.get("return_annotation"): sig += f" -> {self.format_annotation(item['return_annotation'])}" yield f".. py:method:: {sig}" yield f" :canonical: {item['full_name']}" if self.no_index(item): yield " :noindex:" # TODO overloads # TODO collect final decorated in analysis for prop in ("abstractmethod", "async", "classmethod", "final", "staticmethod"): if prop in item.get("properties", []): yield f" :{prop}:" yield "" if self.show_docstring(item): yield f" .. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield ""
[docs] def render_attribute(self, item: ItemData) -> t.Iterable[str]: """Create the content for an attribute.""" yield from self.render_data(item)
[docs] def render_data(self, item: ItemData) -> t.Iterable[str]: """Create the content for a data item.""" short_name = item["full_name"].split(".")[-1] yield f".. py:{item['type']}:: {short_name}" yield f" :canonical: {item['full_name']}" if self.no_index(item): yield " :noindex:" for prop in ("abstractmethod", "classmethod"): if prop in item.get("properties", []): yield f" :{prop}:" if item.get("annotation"): yield f" :type: {self.format_annotation(item['annotation'])}" value = item.get("value") if isinstance(value, str): if len(value.splitlines()) == 1: if len(value) > 100: value = value[:100] + "..." yield f" :value: {value!r}" else: yield " :value: <Multiline-String>" # TODO in sphinx-autoapi, they made a code block inside a details/summary HTML else: value = str(value).replace("\n", " ") if len(value) > 100: value = value[:100] + "..." yield f" :value: {value}" yield "" if self.show_docstring(item): yield f" .. autodoc2-docstring:: {item['full_name']}" if parser_name := self.get_doc_parser(item["full_name"]): yield f" :parser: {parser_name}" yield ""
[docs] def _reformat_cls_base_rst(self, value: str) -> str: """Reformat the base of a class for RST. Base annotations can come in the form:: A[B, C, D] which we want to reformat as:: :py:obj:`A`\\ [\\ :py:obj:`B`\\ , :py:obj:`C`\\ , :py:obj:`D`\\ ] The backslash escapes are needed because of: https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#character-level-inline-markup-1 """ result = "" for sub_target in _RE_DELIMS.split(value.strip()): sub_target = sub_target.strip() if _RE_DELIMS.match(sub_target): result += f"{sub_target}" if sub_target.endswith(","): result += " " else: result += "\\ " elif sub_target: result += f":py:obj:`{self.format_base(sub_target)}`\\ " # Strip off the extra "\ " return result[:-2]