Repository: ManimCommunity/manim

Issue:
Autoaliasattr: `TypeVar`
Manim's autoaliasattr directive does not document `TypeVar`s.
Especially as the amount of type hints in manim increase, this can make it confusing to read the type hint when occasions such as `list[H]` occur, when earlier in the code `H` was defined as `TypeVar("H", bound=Hashable]` and it was simply not documented. 

Potentially, they could be a different section since they typically don't have a description associated with them, and, as such, several typevars can just be in one code block.
The more straightforward option is to just make them all classes, so that references work (similar to how it was done in #3484)


Discussion:

Resolving patch:
diff --git a/manim/utils/docbuild/autoaliasattr_directive.py b/manim/utils/docbuild/autoaliasattr_directive.py
--- a/manim/utils/docbuild/autoaliasattr_directive.py
+++ b/manim/utils/docbuild/autoaliasattr_directive.py
@@ -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()
@@ -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()
 
@@ -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")
@@ -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"])
diff --git a/manim/utils/docbuild/module_parsing.py b/manim/utils/docbuild/module_parsing.py
--- a/manim/utils/docbuild/module_parsing.py
+++ b/manim/utils/docbuild/module_parsing.py
@@ -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
@@ -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
 
@@ -50,7 +58,7 @@
 # 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
@@ -58,19 +66,24 @@ def parse_module_attributes() -> tuple[AliasDocsDict, DataDict]:
 
     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)
@@ -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
@@ -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
 
@@ -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
@@ -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


Test patch: