"""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]