Skip to content

feat(autoaliasattr): Implement Documentation of TypeVar's #3818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions manim/utils/docbuild/autoaliasattr_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
__all__ = ["AliasAttrDocumenter"]


ALIAS_DOCS_DICT, DATA_DICT = parse_module_attributes()
ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT = parse_module_attributes()
ALIAS_LIST = [
alias_name
for module_dict in ALIAS_DOCS_DICT.values()
Expand Down Expand Up @@ -100,10 +100,11 @@ class AliasAttrDocumenter(Directive):

def run(self) -> list[nodes.Element]:
module_name = self.arguments[0]
# Slice module_name[6:] to remove the "manim." prefix which is
# not present in the keys of the DICTs
module_alias_dict = ALIAS_DOCS_DICT.get(module_name[6:], None)
module_attrs_list = DATA_DICT.get(module_name[6:], None)
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()

Expand Down Expand Up @@ -161,6 +162,11 @@ def run(self) -> list[nodes.Element]:
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")
Expand All @@ -172,6 +178,37 @@ def run(self) -> list[nodes.Element]:
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 = ViewList(
[
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"])
Expand Down
49 changes: 41 additions & 8 deletions manim/utils/docbuild/module_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
classified by category in different `AliasCategoryDict` objects.
"""

ModuleTypeVarDict: TypeAlias = dict[str, str]
"""Dictionary containing every :class:`TypeVar` defined in a module."""


AliasDocsDict: TypeAlias = dict[str, ModuleLevelAliasDict]
"""Dictionary which, for every module in Manim, contains documentation
about their module-level attributes which are explicitly defined as
Expand All @@ -39,8 +43,12 @@
explicitly defined as :class:`TypeAlias`.
"""

TypeVarDict: TypeAlias = dict[str, ModuleTypeVarDict]
"""A dictionary mapping module names to dictionaries of :class:`TypeVar` objects."""

ALIAS_DOCS_DICT: AliasDocsDict = {}
DATA_DICT: DataDict = {}
TYPEVAR_DICT: TypeVarDict = {}

MANIM_ROOT = Path(__file__).resolve().parent.parent.parent

Expand All @@ -50,27 +58,32 @@
# ruff: noqa: E721


def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
def parse_module_attributes() -> tuple[AliasDocsDict, DataDict, TypeVarDict]:
"""Read all files, generate Abstract Syntax Trees from them, and
extract useful information about the type aliases defined in the
files: the category they belong to, their definition and their
description, separating them from the "regular" module attributes.

Returns
-------
ALIAS_DOCS_DICT : `AliasDocsDict`
ALIAS_DOCS_DICT : :class:`AliasDocsDict`
A dictionary containing the information from all the type
aliases in Manim. See `AliasDocsDict` for more information.
aliases in Manim. See :class:`AliasDocsDict` for more information.

DATA_DICT : `DataDict`
DATA_DICT : :class:`DataDict`
A dictionary containing the names of all DOCUMENTED
module-level attributes which are not a :class:`TypeAlias`.

TYPEVAR_DICT : :class:`TypeVarDict`
A dictionary containing the definitions of :class:`TypeVar` objects,
organized by modules.
"""
global ALIAS_DOCS_DICT
global DATA_DICT
global TYPEVAR_DICT

if ALIAS_DOCS_DICT or DATA_DICT:
return ALIAS_DOCS_DICT, DATA_DICT
if ALIAS_DOCS_DICT or DATA_DICT or TYPEVAR_DICT:
return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT

for module_path in MANIM_ROOT.rglob("*.py"):
module_name = module_path.resolve().relative_to(MANIM_ROOT)
Expand All @@ -85,6 +98,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
category_dict: AliasCategoryDict | None = None
alias_info: AliasInfo | None = None

# For storing TypeVars
module_typevars: ModuleTypeVarDict = {}

# For storing regular module attributes
data_list: list[str] = []
data_name: str | None = None
Expand Down Expand Up @@ -172,6 +188,19 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
alias_info = category_dict[alias_name]
continue

# Check if it is a typing.TypeVar
elif (
type(node) is ast.Assign
and type(node.targets[0]) is ast.Name
and type(node.value) is ast.Call
and type(node.value.func) is ast.Name
and node.value.func.id.endswith("TypeVar")
):
module_typevars[node.targets[0].id] = ast.unparse(
node.value
).replace("_", r"\_")
continue

# If here, the node is not a TypeAlias definition
alias_info = None

Expand All @@ -185,7 +214,9 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
else:
target = None

if type(target) is ast.Name:
if type(target) is ast.Name and not (
type(node) is ast.Assign and target.id not in module_typevars
):
data_name = target.id
else:
data_name = None
Expand All @@ -194,5 +225,7 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
ALIAS_DOCS_DICT[module_name] = module_dict
if len(data_list) > 0:
DATA_DICT[module_name] = data_list
if module_typevars:
TYPEVAR_DICT[module_name] = module_typevars

return ALIAS_DOCS_DICT, DATA_DICT
return ALIAS_DOCS_DICT, DATA_DICT, TYPEVAR_DICT
Loading