diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71eb10127..10b9a3a85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: version: '0.10.2' - name: Install dependencies - run: uv sync --all-extras + run: uv sync --all-extras --group docs - name: Run lints run: ./scripts/lint diff --git a/docs/conf.py b/docs/conf.py index 42c04c554..4739753c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,15 @@ import os import sys -from typing import Any +import pkgutil +from typing import get_type_hints +from typing_extensions import override + +from sphinx.errors import PycodeError +from sphinx.pycode import ModuleAnalyzer +from sphinx.application import Sphinx +from sphinx.ext.autodoc import Documenter +from sphinx_toolbox.more_autodoc.autotypeddict import TypedDictDocumenter # Add the src directory to the path so we can import the package sys.path.insert(0, os.path.abspath("../src")) @@ -59,7 +67,76 @@ autodoc_typehints_description_target = "documented" -def _inject_type_submodules(_app: Any, docname: str, source: list[str]) -> None: +# -- Autodocumenter extensions ----------------------------------------------- + + +def _collect_field_docstrings(cls: type) -> dict[str, list[str]]: + """Collect field docstrings from cls and all TypedDict ancestors via __orig_bases__.""" + result: dict[str, list[str]] = {} + # Parents first — child docstrings overwrite via later assignment + for base in getattr(cls, "__orig_bases__", ()): + origin = getattr(base, "__origin__", base) + if isinstance(origin, type) and origin is not cls: + result.update(_collect_field_docstrings(origin)) + try: + attr_docs = ModuleAnalyzer.for_module(cls.__module__).find_attr_docs() + for (_, attr_name), doc_lines in attr_docs.items(): + result[attr_name] = doc_lines + except PycodeError: + pass + return result + + +class _InheritedDocsTypedDictDocumenter(TypedDictDocumenter): + """TypedDictDocumenter that collects field docstrings from parent TypedDicts. + + Upstream only scans self.object.__module__ for field docstrings, so + inherited descriptions are lost. This subclass traverses __orig_bases__. + Upstream bug: https://github.com/sphinx-doc/sphinx/issues/9290 + Patching sphinx_toolbox 4.1.2. + """ + + @override + def sort_members( + self, + documenters: list[tuple[Documenter, bool]], + order: str, + ) -> list[tuple[Documenter, bool]]: + # Skip TypedDictDocumenter.sort_members (returns [] after adding + # lines with wrong docstrings). Call ClassDocumenter.sort_members + # to get the properly sorted documenters list. + documenters = super(TypedDictDocumenter, self).sort_members(documenters, order) + docstrings = _collect_field_docstrings(self.object) + required_keys: list[str] = [] + optional_keys: list[str] = [] + types = get_type_hints(self.object) + + for d in documenters: + name = d[0].name.split(".")[-1] + if name in self.object.__required_keys__: + required_keys.append(name) + elif name in self.object.__optional_keys__: + optional_keys.append(name) + + sourcename = self.get_sourcename() + if required_keys: + self.add_line("", sourcename) + self.add_line(":Required Keys:", sourcename) + self.document_keys(required_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] + self.add_line("", sourcename) + if optional_keys: + self.add_line("", sourcename) + self.add_line(":Optional Keys:", sourcename) + self.document_keys(optional_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] + self.add_line("", sourcename) + + return [] + + +# -- Dynamic type documentation ---------------------------------------------- + + +def _inject_type_submodules(_app: Sphinx, docname: str, source: list[str]) -> None: """Auto-generate automodule directives for all type submodules. Replaces the ``.. auto-all-types::`` placeholder in types.rst with @@ -70,8 +147,6 @@ def _inject_type_submodules(_app: Any, docname: str, source: list[str]) -> None: """ if docname != "api/types": return - import pkgutil - import runloop_api_client.types as types_pkg directives: list[str] = [] @@ -85,8 +160,9 @@ def _inject_type_submodules(_app: Any, docname: str, source: list[str]) -> None: source[0] = source[0].replace(".. auto-all-types::", "\n".join(directives)) -def setup(app: Any) -> None: - app.connect("source-read", _inject_type_submodules) +def setup(app: Sphinx) -> None: + app.add_autodocumenter(_InheritedDocsTypedDictDocumenter, override=True) + app.connect("source-read", _inject_type_submodules) # pyright: ignore[reportUnknownMemberType] # Intersphinx mapping diff --git a/pyproject.toml b/pyproject.toml index 52d3431d2..2c3dbdb53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,6 +72,7 @@ docs = [ "furo>=2025.9.25", "sphinx>=7.4.7", "sphinx-autodoc-typehints>=2.3.0", + "sphinx-tabs>=3.4.0", "sphinx-toolbox>=4.0.0", ] diff --git a/uv.lock b/uv.lock index 15f6eb6a7..7f63794ea 100644 --- a/uv.lock +++ b/uv.lock @@ -2435,6 +2435,7 @@ docs = [ { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx-autodoc-typehints", version = "3.9.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-tabs" }, { name = "sphinx-toolbox" }, ] @@ -2474,6 +2475,7 @@ docs = [ { name = "furo", specifier = ">=2025.9.25" }, { name = "sphinx", specifier = ">=7.4.7" }, { name = "sphinx-autodoc-typehints", specifier = ">=2.3.0" }, + { name = "sphinx-tabs", specifier = ">=3.4.0" }, { name = "sphinx-toolbox", specifier = ">=4.0.0" }, ]