Skip to content

Commit 6836975

Browse files
committed
feat(docs[_ext]): Link return type annotations to model and Python docs
why: "Returns: PaneInfo" rendered as plain code text with no hyperlink. All model classes are in objects.inv and should be clickable. what: - Add _make_type_xref() that creates pending_xref nodes for type names - Known model classes (PaneInfo, etc.) resolve to reference/api/models/ - Builtins (str, list) resolve to Python docs via intersphinx - Handle list[X] generics with separate xrefs for container and inner - Replace _make_literal() with _make_type_xref() for return annotations - Add 4 tests: model class, list[model], builtin, unknown type
1 parent 01319d1 commit 6836975

2 files changed

Lines changed: 113 additions & 4 deletions

File tree

docs/_ext/fastmcp_autodoc.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from dataclasses import dataclass
3131

3232
from docutils import nodes
33+
from sphinx import addnodes
3334
from sphinx.application import Sphinx
3435
from sphinx.util.docutils import SphinxDirective
3536

@@ -60,6 +61,20 @@
6061
TAG_MUTATING = "mutating"
6162
TAG_DESTRUCTIVE = "destructive"
6263

64+
_MODEL_MODULE = "libtmux_mcp.models"
65+
_MODEL_CLASSES: set[str] = {
66+
"SessionInfo",
67+
"WindowInfo",
68+
"PaneInfo",
69+
"PaneContentMatch",
70+
"ServerInfo",
71+
"OptionResult",
72+
"OptionSetResult",
73+
"EnvironmentResult",
74+
"EnvironmentSetResult",
75+
"WaitForTextResult",
76+
}
77+
6378

6479
# ---------------------------------------------------------------------------
6580
# Data classes
@@ -278,6 +293,42 @@ def _make_literal(text: str) -> nodes.literal:
278293
return nodes.literal("", text)
279294

280295

296+
def _single_type_xref(name: str) -> addnodes.pending_xref:
297+
"""Create a ``pending_xref`` for a single type name.
298+
299+
Known model classes are qualified to ``libtmux_mcp.models.X``.
300+
Builtins (``str``, ``list``, ``int``, etc.) target the Python domain.
301+
"""
302+
target = f"{_MODEL_MODULE}.{name}" if name in _MODEL_CLASSES else name
303+
return addnodes.pending_xref(
304+
"",
305+
nodes.literal("", name),
306+
refdomain="py",
307+
reftype="class",
308+
reftarget=target,
309+
)
310+
311+
312+
def _make_type_xref(type_str: str) -> nodes.paragraph:
313+
"""Render a return type annotation with cross-reference links.
314+
315+
Handles ``list[X]`` generics and bare type names.
316+
Each type component becomes a ``pending_xref`` that Sphinx resolves
317+
into a hyperlink (internal or intersphinx).
318+
"""
319+
para = nodes.paragraph("")
320+
m = re.match(r"^(list|set|tuple)\[(.+)\]$", type_str)
321+
if m:
322+
container, inner = m.group(1), m.group(2)
323+
para += _single_type_xref(container)
324+
para += nodes.Text("[")
325+
para += _single_type_xref(inner)
326+
para += nodes.Text("]")
327+
else:
328+
para += _single_type_xref(type_str)
329+
return para
330+
331+
281332
def _make_para(*children: nodes.Node | str) -> nodes.paragraph:
282333
"""Create a paragraph from mixed text and node children."""
283334
para = nodes.paragraph("")
@@ -533,10 +584,12 @@ def _build_tool_section(self, tool: ToolInfo) -> list[nodes.Node]:
533584

534585
# Returns (promoted — high-signal for tool selection)
535586
if tool.return_annotation:
536-
section += _make_para(
537-
nodes.strong("", "Returns: "),
538-
_make_literal(tool.return_annotation),
539-
)
587+
returns_para = nodes.paragraph("")
588+
returns_para += nodes.strong("", "Returns: ")
589+
type_para = _make_type_xref(tool.return_annotation)
590+
for child in type_para.children:
591+
returns_para += child.deepcopy()
592+
section += returns_para
540593

541594
return [section]
542595

tests/docs/_ext/test_fastmcp_autodoc.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,62 @@ def test_safety_badge_classes() -> None:
443443
assert "sd-bg-danger" in badge["classes"]
444444

445445

446+
# ---------------------------------------------------------------------------
447+
# _make_type_xref
448+
# ---------------------------------------------------------------------------
449+
450+
451+
def test_make_type_xref_model_class() -> None:
452+
"""_make_type_xref creates pending_xref for known model classes."""
453+
from sphinx import addnodes
454+
455+
para = fastmcp_autodoc._make_type_xref("PaneInfo")
456+
assert len(para.children) == 1
457+
xref = para.children[0]
458+
assert isinstance(xref, addnodes.pending_xref)
459+
assert xref["refdomain"] == "py"
460+
assert xref["reftype"] == "class"
461+
assert xref["reftarget"] == "libtmux_mcp.models.PaneInfo"
462+
assert xref.children[0].astext() == "PaneInfo"
463+
464+
465+
def test_make_type_xref_list_of_model() -> None:
466+
"""_make_type_xref handles list[SessionInfo] with xrefs for both."""
467+
from sphinx import addnodes
468+
469+
para = fastmcp_autodoc._make_type_xref("list[SessionInfo]")
470+
# list xref, "[", SessionInfo xref, "]"
471+
assert len(para.children) == 4
472+
list_xref = para.children[0]
473+
assert isinstance(list_xref, addnodes.pending_xref)
474+
assert list_xref["reftarget"] == "list"
475+
476+
inner_xref = para.children[2]
477+
assert isinstance(inner_xref, addnodes.pending_xref)
478+
assert inner_xref["reftarget"] == "libtmux_mcp.models.SessionInfo"
479+
480+
481+
def test_make_type_xref_builtin() -> None:
482+
"""_make_type_xref creates pending_xref for builtins like str."""
483+
from sphinx import addnodes
484+
485+
para = fastmcp_autodoc._make_type_xref("str")
486+
xref = para.children[0]
487+
assert isinstance(xref, addnodes.pending_xref)
488+
assert xref["reftarget"] == "str"
489+
assert xref["refdomain"] == "py"
490+
491+
492+
def test_make_type_xref_unknown() -> None:
493+
"""_make_type_xref still creates pending_xref for unknown types."""
494+
from sphinx import addnodes
495+
496+
para = fastmcp_autodoc._make_type_xref("SomeUnknown")
497+
xref = para.children[0]
498+
assert isinstance(xref, addnodes.pending_xref)
499+
assert xref["reftarget"] == "SomeUnknown"
500+
501+
446502
# ---------------------------------------------------------------------------
447503
# SECTION_BADGE_MAP + _add_section_badges
448504
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)