Skip to content

Commit 578233b

Browse files
nficanoclaude
andcommitted
chore: add Markdown API doc generation tooling
Add a docs-api Makefile target, scripts/gen_api_docs.py, and pydoc-markdown.yml to render the API reference into docs/api/; gitignore the generated output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df0f524 commit 578233b

4 files changed

Lines changed: 205 additions & 1 deletion

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ build/
1212
.ruff_cache/
1313
.pyright/
1414
.DS_Store
15+
16+
# Generated API docs (see Makefile target `docs-api`). Re-rendered from
17+
# docstrings by scripts/gen_api_docs.py; the site at ../www builds
18+
# fresh from python-sdk/docs/**/*.md, so we don't track the output here.
19+
docs/api/

Makefile

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: diagrams build clean publish test
1+
.PHONY: diagrams build clean publish test docs-api
22

33
DIAGRAMS := arch-overview session-lifecycle job-lifecycle capability-negotiation heartbeat-ack result-chunk-progress
44
DIAGRAM_DIR := docs/diagrams
@@ -30,3 +30,19 @@ publish: build
3030
# but uses `uv run` so the lockfile is honored.
3131
test:
3232
uv run pytest --cov --cov-branch --cov-report=xml
33+
34+
# Generate Markdown API docs from docstrings into docs/api/.
35+
# The site at ../www ingests `<lang>-sdk/docs/**/*.md` at build time,
36+
# so this target writes one .md per module under docs/api/. Output is
37+
# .gitignored; re-run whenever public API docstrings change.
38+
#
39+
# Requires pydoc-markdown on PATH with arcp's runtime deps injected:
40+
# pipx install pydoc-markdown
41+
# pipx inject pydoc-markdown pydantic structlog python-ulid websockets \
42+
# click aiosqlite 'pyjwt[crypto]' opentelemetry-api httpx
43+
docs-api:
44+
@command -v pydoc-markdown >/dev/null 2>&1 || { \
45+
echo "error: pydoc-markdown not found; install with: pipx install pydoc-markdown"; \
46+
exit 1; \
47+
}
48+
python3 scripts/gen_api_docs.py

pydoc-markdown.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Base pydoc-markdown configuration for arcp.
2+
#
3+
# This is the shared loader/processor block consumed by scripts/gen_api_docs.py.
4+
# The script iterates over every public module under src/arcp, overrides the
5+
# `modules` list per-invocation, and writes the rendered output to
6+
# docs/api/<dotted.path>.md so the site generator at ../www can ingest it as
7+
# `<lang>-sdk/docs/**/*.md`.
8+
loaders:
9+
- type: python
10+
search_path: [src]
11+
processors:
12+
# Drop `from x import y` re-exports (docspec calls these Indirection)
13+
# so module pages don't list every imported name as if it were a
14+
# module-level definition. Keep dunder methods (`__init__`, etc.)
15+
# because they carry useful signatures. `do_not_filter_modules`
16+
# keeps the module nodes themselves even when their content is
17+
# otherwise filtered out (e.g. arcp.client which is purely
18+
# re-exports).
19+
- type: filter
20+
expression: obj.__class__.__name__ != 'Indirection' and name not in ('__all__', '__path__', '__annotations__', '__name__') and (not name.startswith('_') or (name.startswith('__') and name.endswith('__')))
21+
documented_only: false
22+
do_not_filter_modules: true
23+
skip_empty_modules: false
24+
- type: smart
25+
- type: crossref
26+
renderer:
27+
type: markdown
28+
render_module_header: true
29+
render_toc: true
30+
render_toc_title: ''
31+
insert_header_anchors: false
32+
descriptive_class_title: false
33+
add_method_class_prefix: true
34+
add_member_class_prefix: true
35+
classdef_with_decorators: true
36+
signature_with_decorators: true
37+
signature_class_prefix: true
38+
signature_with_def: true
39+
signature_python_help_style: false
40+
# yapf in pydoc-markdown 4.8 does not parse some modern Python syntax
41+
# (e.g. PEP 695 type params, certain default expressions) and will
42+
# crash with `YapfError: expected '('`. We disable yapf-based
43+
# signature reflow; pydoc-markdown still emits a clean ```python
44+
# fenced block.
45+
format_code: false
46+
use_fixed_header_levels: true
47+
header_level_by_type:
48+
Module: 1
49+
Class: 2
50+
Method: 3
51+
Function: 2
52+
Variable: 3
53+
Data: 3

scripts/gen_api_docs.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
"""Generate Markdown API docs for arcp.
3+
4+
For every Python module under ``src/arcp``, this script runs
5+
``pydoc-markdown`` once and writes the rendered Markdown to
6+
``docs/api/<dotted.module.path>.md`` -- one file per module. The site
7+
at ``../www`` ingests ``python-sdk/docs/**/*.md`` at build time, so
8+
the output deliberately stays as flat Markdown with no framework
9+
trappings (no Hugo/Docusaurus front matter, no HTML).
10+
11+
Usage:
12+
python3 scripts/gen_api_docs.py
13+
14+
The shared loader/processor/renderer config lives in
15+
``pydoc-markdown.yml`` next to this script's parent. We invoke
16+
``pydoc-markdown <inline-yaml>`` per module so each invocation only
17+
emits one module's documentation; that gives us one ``.md`` per
18+
module without depending on the mkdocs/docusaurus multi-page
19+
renderers, which would drag in framework-specific output.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import shutil
25+
import subprocess
26+
import sys
27+
from pathlib import Path
28+
29+
ROOT = Path(__file__).resolve().parent.parent
30+
SRC = ROOT / "src" / "arcp"
31+
OUT = ROOT / "docs" / "api"
32+
CONFIG = ROOT / "pydoc-markdown.yml"
33+
34+
35+
def discover_modules() -> list[str]:
36+
"""Return every importable module under ``src/arcp`` as dotted paths."""
37+
modules: list[str] = []
38+
for path in sorted(SRC.rglob("*.py")):
39+
rel = path.relative_to(SRC.parent) # e.g. arcp/_client/client.py
40+
parts = list(rel.with_suffix("").parts)
41+
if parts[-1] == "__init__":
42+
parts = parts[:-1]
43+
if not parts or parts[-1] == "__main__":
44+
# Skip __main__ (entrypoint shim, no real API surface) and any
45+
# bare package root that collapsed to nothing.
46+
if parts and parts[-1] == "__main__":
47+
continue
48+
if not parts:
49+
continue
50+
modules.append(".".join(parts))
51+
# Deduplicate while preserving order.
52+
seen: set[str] = set()
53+
unique: list[str] = []
54+
for m in modules:
55+
if m not in seen:
56+
seen.add(m)
57+
unique.append(m)
58+
return unique
59+
60+
61+
def render(module: str) -> str:
62+
"""Run pydoc-markdown for a single module and return its Markdown."""
63+
# Inline YAML override: keep the shared config but pin `modules` to one
64+
# name. pydoc-markdown accepts a YAML string as the CONFIG arg.
65+
config_yaml = CONFIG.read_text(encoding="utf-8")
66+
# Append a `modules` entry to the python loader. The base config has
67+
# one loader with `search_path: [src]`; we add `modules: [...]` to it.
68+
override = config_yaml.replace(
69+
" search_path: [src]",
70+
f" search_path: [src]\n modules: ['{module}']",
71+
1,
72+
)
73+
result = subprocess.run(
74+
["pydoc-markdown", override],
75+
cwd=ROOT,
76+
check=True,
77+
capture_output=True,
78+
text=True,
79+
)
80+
return result.stdout
81+
82+
83+
def main() -> int:
84+
if shutil.which("pydoc-markdown") is None:
85+
print(
86+
"error: `pydoc-markdown` not on PATH. Install with "
87+
"`pipx install pydoc-markdown` and run `pipx inject "
88+
"pydoc-markdown <runtime deps>` so arcp is importable.",
89+
file=sys.stderr,
90+
)
91+
return 1
92+
93+
if OUT.exists():
94+
shutil.rmtree(OUT)
95+
OUT.mkdir(parents=True, exist_ok=True)
96+
97+
modules = discover_modules()
98+
if not modules:
99+
print("error: no modules discovered under src/arcp", file=sys.stderr)
100+
return 1
101+
102+
print(f"Rendering {len(modules)} modules -> {OUT.relative_to(ROOT)}/")
103+
for module in modules:
104+
target = OUT / f"{module}.md"
105+
target.parent.mkdir(parents=True, exist_ok=True)
106+
try:
107+
markdown = render(module)
108+
except subprocess.CalledProcessError as exc:
109+
print(f" FAIL {module}", file=sys.stderr)
110+
print(exc.stderr, file=sys.stderr)
111+
return exc.returncode
112+
if not markdown.strip():
113+
print(f" skip {module} (empty)")
114+
continue
115+
target.write_text(markdown, encoding="utf-8")
116+
print(f" ok {module} -> {target.relative_to(ROOT)}")
117+
118+
# Write an index so the site has a stable landing page for /api/.
119+
index = OUT / "README.md"
120+
lines = ["# Python SDK API reference", "", "Auto-generated from docstrings in `src/arcp`.", ""]
121+
for module in modules:
122+
md_path = f"{module}.md"
123+
lines.append(f"- [`{module}`]({md_path})")
124+
index.write_text("\n".join(lines) + "\n", encoding="utf-8")
125+
print(f" ok index -> {index.relative_to(ROOT)}")
126+
return 0
127+
128+
129+
if __name__ == "__main__":
130+
sys.exit(main())

0 commit comments

Comments
 (0)