Source code for manim.utils.docbuild.autoaliasattr_directive

"""A directive for documenting type aliases and other module-level attributes."""

from __future__ import annotations

from typing import TYPE_CHECKING

from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.statemachine import StringList

from manim.utils.docbuild.module_parsing import parse_module_attributes

if TYPE_CHECKING:
    from sphinx.application import Sphinx

__all__ = ["AliasAttrDocumenter"]


ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes()
ALIAS_LIST = [
    alias_name
    for module_dict in ALIAS_DOCS_DICT.values()
    for category_dict in module_dict.values()
    for alias_name in category_dict
]


[docs] def smart_replace(base: str, alias: str, substitution: str) -> str: """Auxiliary function for substituting type aliases into a base string, when there are overlaps between the aliases themselves. Parameters ---------- base The string in which the type aliases will be located and replaced. alias The substring to be substituted. substitution The string which will replace every occurrence of ``alias``. Returns ------- str The new string after the alias substitution. """ occurrences = [] len_alias = len(alias) len_base = len(base) def condition(char: str) -> bool: return not char.isalnum() and char != "_" start = 0 i = 0 while True: i = base.find(alias, start) if i == -1: break if (i == 0 or condition(base[i - 1])) and ( i + len_alias == len_base or condition(base[i + len_alias]) ): occurrences.append(i) start = i + len_alias for o in occurrences[::-1]: base = base[:o] + substitution + base[o + len_alias :] return base
[docs] def setup(app: Sphinx) -> None: app.add_directive("autoaliasattr", AliasAttrDocumenter)
[docs] class AliasAttrDocumenter(Directive): """Directive which replaces Sphinx's Autosummary for module-level attributes: instead, it manually crafts a new "Type Aliases" section, where all the module-level attributes which are explicitly annotated as :class:`TypeAlias` are considered as such, for their use all around the Manim docs. These type aliases are separated from the "regular" module-level attributes, which get their traditional "Module Attributes" section autogenerated with Sphinx's Autosummary under "Type Aliases". See ``docs/source/_templates/autosummary/module.rst`` to watch this directive in action. See :func:`~.parse_module_attributes` for more information on how the modules are parsed to obtain the :class:`TypeAlias` information and separate it from the other attributes. """ objtype = "autoaliasattr" required_arguments = 1 has_content = True def run(self) -> list[nodes.Element]: module_name = self.arguments[0] # not present in the keys of the DICTs module_name = module_name.removeprefix("manim.") module_alias_dict = ALIAS_DOCS_DICT.get(module_name, None) module_attrs_list = DATA_DICT.get(module_name, None) module_typevars = TYPEVAR_DICT.get(module_name, None) content = nodes.container() # Add "Type Aliases" section if module_alias_dict is not None: module_alias_section = nodes.section(ids=[f"{module_name}.alias"]) content += module_alias_section # Use a rubric (title-like), just like in `module.rst` module_alias_section += nodes.rubric(text="Type Aliases") # category_name: str # category_dict: AliasCategoryDict = dict[str, AliasInfo] for category_name, category_dict in module_alias_dict.items(): category_section = nodes.section( ids=[category_name.lower().replace(" ", "_")] ) module_alias_section += category_section # category_name can be possibly "" for uncategorized aliases if category_name: category_section += nodes.title(text=category_name) category_alias_container = nodes.container() category_section += category_alias_container # alias_name: str # alias_info: AliasInfo = dict[str, str] # Contains "definition": str # Can possibly contain "doc": str for alias_name, alias_info in category_dict.items(): # Replace all occurrences of type aliases in the # definition for automatic cross-referencing! alias_def = alias_info["definition"] for A in ALIAS_LIST: alias_def = smart_replace(alias_def, A, f":class:`~.{A}`") # Using the `.. class::` directive is CRUCIAL, since # function/method parameters are always annotated via # classes - therefore Sphinx expects a class unparsed = StringList( [ f".. class:: {alias_name}", "", " .. parsed-literal::", "", f" {alias_def}", "", ] ) if "doc" in alias_info: # Replace all occurrences of type aliases in # the docs for automatic cross-referencing! alias_doc = alias_info["doc"] for A in ALIAS_LIST: alias_doc = alias_doc.replace(f"`{A}`", f":class:`~.{A}`") # also hyperlink the TypeVars from that module if module_typevars is not None: for T in module_typevars: alias_doc = alias_doc.replace(f"`{T}`", f":class:`{T}`") # Add all the lines with 4 spaces behind, to consider all the # documentation as a paragraph INSIDE the `.. class::` block doc_lines = alias_doc.split("\n") unparsed.extend( StringList([f" {line}" for line in doc_lines]) ) # Parse the reST text into a fresh container # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest alias_container = nodes.container() self.state.nested_parse(unparsed, 0, alias_container) category_alias_container += alias_container # then add the module TypeVars section if module_typevars is not None: module_typevars_section = nodes.section(ids=[f"{module_name}.typevars"]) content += module_typevars_section # Use a rubric (title-like), just like in `module.rst` module_typevars_section += nodes.rubric(text="TypeVar's") # name: str # definition: TypeVarDict = dict[str, str] for name, definition in module_typevars.items(): # Using the `.. class::` directive is CRUCIAL, since # function/method parameters are always annotated via # classes - therefore Sphinx expects a class unparsed = StringList( [ f".. class:: {name}", "", " .. parsed-literal::", "", f" {definition}", "", ] ) # Parse the reST text into a fresh container # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest typevar_container = nodes.container() self.state.nested_parse(unparsed, 0, typevar_container) module_typevars_section += typevar_container # Then, add the traditional "Module Attributes" section if module_attrs_list is not None: module_attrs_section = nodes.section(ids=[f"{module_name}.data"]) content += module_attrs_section # Use the same rubric (title-like) as in `module.rst` module_attrs_section += nodes.rubric(text="Module Attributes") # Let Sphinx Autosummary do its thing as always # Add all the attribute names with 4 spaces behind, so that # they're considered as INSIDE the `.. autosummary::` block unparsed = StringList( [ ".. autosummary::", *(f" {attr}" for attr in module_attrs_list), ] ) # Parse the reST text into a fresh container # https://www.sphinx-doc.org/en/master/extdev/markupapi.html#parsing-directive-content-as-rest data_container = nodes.container() self.state.nested_parse(unparsed, 0, data_container) module_attrs_section += data_container return [content]