From b60d8678a0d61c997f53f82eb48b34d7e6011b82 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Fri, 10 Apr 2026 13:26:49 -0700 Subject: [PATCH 1/5] docs: show full field descriptions for SDK parameter types --- docs/conf.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 42c04c554..ebf2f0748 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,7 +7,8 @@ import os import sys -from typing import Any +import pkgutil +from typing import Any, get_type_hints # Add the src directory to the path so we can import the package sys.path.insert(0, os.path.abspath("../src")) @@ -59,6 +60,87 @@ autodoc_typehints_description_target = "documented" +# -- Patches ----------------------------------------------------------------- + + +def _patch_autotypeddict_inherited_docstrings() -> None: + """Patch autotypeddict to include field docstrings from parent TypedDicts. + + sphinx_toolbox's sort_members only scans self.object.__module__ for field + docstrings (autotypeddict.py:381-384). SDK TypedDicts like SDKDevboxCreateParams + inherit fields from types in other modules (e.g. DevboxCreateParams), so those + descriptions are lost. This patch collects docstrings from the full + __orig_bases__ chain. + + Upstream bug: https://github.com/sphinx-doc/sphinx/issues/9290 + """ + from sphinx.errors import PycodeError + from sphinx.pycode import ModuleAnalyzer + from sphinx_toolbox.more_autodoc.autotypeddict import TypedDictDocumenter + + def _collect_field_docstrings(cls: type) -> dict[str, list[str]]: + result: dict[str, list[str]] = {} + visited: set[type] = set() + + def _walk(klass: type) -> None: + if klass in visited or not hasattr(klass, "__annotations__"): + return + visited.add(klass) + try: + for (_, fname), doc in ModuleAnalyzer.for_module(klass.__module__).find_attr_docs().items(): + if fname not in result: + result[fname] = doc + except PycodeError: + pass + for base in getattr(klass, "__orig_bases__", []): + if isinstance(base, type): + _walk(base) + + _walk(cls) + return result + + def _patched_sort_members( + self: TypedDictDocumenter, + documenters: list[tuple[Any, bool]], + order: str, + ) -> list[tuple[Any, bool]]: + 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 [] + + TypedDictDocumenter.sort_members = _patched_sort_members # type: ignore[assignment] + + +_patch_autotypeddict_inherited_docstrings() + + +# -- Dynamic type documentation ---------------------------------------------- + + def _inject_type_submodules(_app: Any, docname: str, source: list[str]) -> None: """Auto-generate automodule directives for all type submodules. @@ -70,8 +152,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] = [] From 71bebe882007fcae9dc662795b9a168f864fe389 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Fri, 10 Apr 2026 13:33:36 -0700 Subject: [PATCH 2/5] exclude `docs/` from pyright checks --- docs/conf.py | 23 +++++++++++++---------- pyproject.toml | 1 + 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ebf2f0748..a6c835b10 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,13 @@ import os import sys import pkgutil -from typing import Any, get_type_hints +from typing import get_type_hints + +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")) @@ -74,9 +80,6 @@ def _patch_autotypeddict_inherited_docstrings() -> None: Upstream bug: https://github.com/sphinx-doc/sphinx/issues/9290 """ - from sphinx.errors import PycodeError - from sphinx.pycode import ModuleAnalyzer - from sphinx_toolbox.more_autodoc.autotypeddict import TypedDictDocumenter def _collect_field_docstrings(cls: type) -> dict[str, list[str]]: result: dict[str, list[str]] = {} @@ -101,9 +104,9 @@ def _walk(klass: type) -> None: def _patched_sort_members( self: TypedDictDocumenter, - documenters: list[tuple[Any, bool]], + documenters: list[tuple[Documenter, bool]], order: str, - ) -> list[tuple[Any, bool]]: + ) -> list[tuple[Documenter, bool]]: documenters = super(TypedDictDocumenter, self).sort_members(documenters, order) docstrings = _collect_field_docstrings(self.object) @@ -122,12 +125,12 @@ def _patched_sort_members( if required_keys: self.add_line("", sourcename) self.add_line(":Required Keys:", sourcename) - self.document_keys(required_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] + self.document_keys(required_keys, types, docstrings) 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.document_keys(optional_keys, types, docstrings) self.add_line("", sourcename) return [] @@ -141,7 +144,7 @@ def _patched_sort_members( # -- Dynamic type documentation ---------------------------------------------- -def _inject_type_submodules(_app: Any, docname: str, source: list[str]) -> None: +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 @@ -165,7 +168,7 @@ 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: +def setup(app: Sphinx) -> None: app.connect("source-read", _inject_type_submodules) diff --git a/pyproject.toml b/pyproject.toml index 52d3431d2..a8371c40f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,7 @@ exclude = [ "_dev", ".venv", ".git", + "docs", ] reportImplicitOverride = true From 1eadd02ecddf1ec50edfae99a5a903fcf353b597 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Fri, 10 Apr 2026 13:56:49 -0700 Subject: [PATCH 3/5] re-add docs to pyright, add targeted ignores --- docs/conf.py | 7 ++++--- pyproject.toml | 7 ++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a6c835b10..ae19d9b5f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,4 @@ +# pyright: reportMissingImports=false # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -125,12 +126,12 @@ def _patched_sort_members( if required_keys: self.add_line("", sourcename) self.add_line(":Required Keys:", sourcename) - self.document_keys(required_keys, types, docstrings) + 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) + self.document_keys(optional_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] self.add_line("", sourcename) return [] @@ -169,7 +170,7 @@ def _inject_type_submodules(_app: Sphinx, docname: str, source: list[str]) -> No def setup(app: Sphinx) -> None: - app.connect("source-read", _inject_type_submodules) + app.connect("source-read", _inject_type_submodules) # pyright: ignore[reportUnknownMemberType] # Intersphinx mapping diff --git a/pyproject.toml b/pyproject.toml index a8371c40f..4d2258207 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,7 +138,6 @@ exclude = [ "_dev", ".venv", ".git", - "docs", ] reportImplicitOverride = true @@ -198,6 +197,12 @@ module = "black.files.*" ignore_errors = true ignore_missing_imports = true +# Sphinx packages are only installed in the docs dependency group, +# not in the dev group used by CI linters. +[[tool.mypy.overrides]] +module = ["sphinx.*", "sphinx_toolbox.*"] +ignore_missing_imports = true + [tool.ruff] line-length = 120 From 082f3ef0a3f12149c83df0bba8b0a6c4e5860a21 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Fri, 10 Apr 2026 14:55:54 -0700 Subject: [PATCH 4/5] extend autodocumenter instead of monkeypatching (cleaner approach), add back `docs/` to pyright ignored --- docs/conf.py | 72 ++++++++++++++++++++++---------------------------- pyproject.toml | 1 + 2 files changed, 32 insertions(+), 41 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ae19d9b5f..307900d13 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# pyright: reportMissingImports=false # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -67,50 +66,45 @@ autodoc_typehints_description_target = "documented" -# -- Patches ----------------------------------------------------------------- +# -- Autodocumenter extensions ----------------------------------------------- -def _patch_autotypeddict_inherited_docstrings() -> None: - """Patch autotypeddict to include field docstrings from parent TypedDicts. +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 - sphinx_toolbox's sort_members only scans self.object.__module__ for field - docstrings (autotypeddict.py:381-384). SDK TypedDicts like SDKDevboxCreateParams - inherit fields from types in other modules (e.g. DevboxCreateParams), so those - descriptions are lost. This patch collects docstrings from the full - __orig_bases__ chain. +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. """ - def _collect_field_docstrings(cls: type) -> dict[str, list[str]]: - result: dict[str, list[str]] = {} - visited: set[type] = set() - - def _walk(klass: type) -> None: - if klass in visited or not hasattr(klass, "__annotations__"): - return - visited.add(klass) - try: - for (_, fname), doc in ModuleAnalyzer.for_module(klass.__module__).find_attr_docs().items(): - if fname not in result: - result[fname] = doc - except PycodeError: - pass - for base in getattr(klass, "__orig_bases__", []): - if isinstance(base, type): - _walk(base) - - _walk(cls) - return result - - def _patched_sort_members( - self: TypedDictDocumenter, + 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) @@ -126,21 +120,16 @@ def _patched_sort_members( if required_keys: self.add_line("", sourcename) self.add_line(":Required Keys:", sourcename) - self.document_keys(required_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] + self.document_keys(required_keys, types, docstrings) 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.document_keys(optional_keys, types, docstrings) self.add_line("", sourcename) return [] - TypedDictDocumenter.sort_members = _patched_sort_members # type: ignore[assignment] - - -_patch_autotypeddict_inherited_docstrings() - # -- Dynamic type documentation ---------------------------------------------- @@ -170,7 +159,8 @@ def _inject_type_submodules(_app: Sphinx, docname: str, source: list[str]) -> No def setup(app: Sphinx) -> None: - app.connect("source-read", _inject_type_submodules) # pyright: ignore[reportUnknownMemberType] + app.add_autodocumenter(_InheritedDocsTypedDictDocumenter, override=True) + app.connect("source-read", _inject_type_submodules) # Intersphinx mapping diff --git a/pyproject.toml b/pyproject.toml index 4d2258207..f7d3dd167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,7 @@ exclude = [ "_dev", ".venv", ".git", + "docs", ] reportImplicitOverride = true From 0641d7a5eac0abed62239e36c4ee3e38621aad3d Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Fri, 10 Apr 2026 15:25:00 -0700 Subject: [PATCH 5/5] add docs group to `uv` dependencies for ci lint check --- .github/workflows/ci.yml | 2 +- docs/conf.py | 8 +++++--- pyproject.toml | 8 +------- uv.lock | 2 ++ 4 files changed, 9 insertions(+), 11 deletions(-) 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 307900d13..4739753c2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,6 +9,7 @@ import sys import pkgutil from typing import get_type_hints +from typing_extensions import override from sphinx.errors import PycodeError from sphinx.pycode import ModuleAnalyzer @@ -95,6 +96,7 @@ class _InheritedDocsTypedDictDocumenter(TypedDictDocumenter): Patching sphinx_toolbox 4.1.2. """ + @override def sort_members( self, documenters: list[tuple[Documenter, bool]], @@ -120,12 +122,12 @@ def sort_members( if required_keys: self.add_line("", sourcename) self.add_line(":Required Keys:", sourcename) - self.document_keys(required_keys, types, docstrings) + 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) + self.document_keys(optional_keys, types, docstrings) # pyright: ignore[reportUnknownMemberType] self.add_line("", sourcename) return [] @@ -160,7 +162,7 @@ def _inject_type_submodules(_app: Sphinx, docname: str, source: list[str]) -> No def setup(app: Sphinx) -> None: app.add_autodocumenter(_InheritedDocsTypedDictDocumenter, override=True) - app.connect("source-read", _inject_type_submodules) + app.connect("source-read", _inject_type_submodules) # pyright: ignore[reportUnknownMemberType] # Intersphinx mapping diff --git a/pyproject.toml b/pyproject.toml index f7d3dd167..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", ] @@ -138,7 +139,6 @@ exclude = [ "_dev", ".venv", ".git", - "docs", ] reportImplicitOverride = true @@ -198,12 +198,6 @@ module = "black.files.*" ignore_errors = true ignore_missing_imports = true -# Sphinx packages are only installed in the docs dependency group, -# not in the dev group used by CI linters. -[[tool.mypy.overrides]] -module = ["sphinx.*", "sphinx_toolbox.*"] -ignore_missing_imports = true - [tool.ruff] line-length = 120 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" }, ]