gabriel / muse public
doc_renderer.py python
657 lines 23.3 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Multi-format documentation renderer for ``muse code docs``.
2
3 Converts a :class:`~muse.core.doc_extractor.DocReport` into one of four
4 output formats:
5
6 ``json``
7 Machine-readable, AI-ready. The full :class:`DocReport` TypedDict
8 serialised as pretty-printed JSON. Suitable for ingestion by LLMs,
9 RAG pipelines, and downstream tooling.
10
11 ``html``
12 Standalone HTML page with an inline sidebar TOC, health bars, version
13 badges, caller/callee sections, and linked test lists. Zero external
14 dependencies — a single file that opens in any browser.
15
16 ``markdown``
17 GitHub-compatible Markdown with a generated table of contents, one
18 ``##`` heading per symbol, and structured sections for callers, callees,
19 version history, and test linkage.
20
21 ``text``
22 Terminal-friendly columnar table similar to ``muse code symbols``, with
23 health score, stale indicator, and doc-debt summary.
24
25 Security
26 --------
27 All user-visible strings (symbol names, docstrings, file paths) are escaped
28 appropriately before embedding in HTML or JSON output. The renderer never
29 evaluates or executes any string from the report.
30
31 Performance
32 -----------
33 Renderers operate in a single linear pass over the ``symbols`` list.
34 On a 400-symbol report the HTML renderer completes in <10 ms.
35 """
36
37 from __future__ import annotations
38
39 import html as _html_module
40 import json
41 import logging
42 from typing import Literal, TypedDict
43
44 from muse.core.doc_extractor import DocReport, DocSummary, SymbolDoc
45
46
47 type _BadgeMap = dict[str, str]
48 logger = logging.getLogger(__name__)
49
50 RenderFormat = Literal["json", "html", "markdown", "text"]
51 """Supported output formats for :func:`render`."""
52
53
54 # ---------------------------------------------------------------------------
55 # JSON renderer
56 # ---------------------------------------------------------------------------
57
58
59 def render_json(report: DocReport) -> str:
60 """Return the full *report* as pretty-printed JSON.
61
62 The output is structured identically to the :class:`DocReport` TypedDict —
63 every field is present, making it directly usable by LLMs and agent
64 pipelines.
65
66 Args:
67 report: The documentation report to serialise.
68 """
69 return json.dumps(report, indent=2, ensure_ascii=False)
70
71
72 # ---------------------------------------------------------------------------
73 # Text renderer
74 # ---------------------------------------------------------------------------
75
76 _HEALTH_BAR_WIDTH = 10
77
78
79 def _health_bar(score: float) -> str:
80 """Return a compact ASCII health bar, e.g. ``[████████░░]``."""
81 filled = round(score * _HEALTH_BAR_WIDTH)
82 empty = _HEALTH_BAR_WIDTH - filled
83 return "[" + "█" * filled + "░" * empty + "]"
84
85
86 def _stale_flag(doc: SymbolDoc) -> str:
87 return "⚠" if "stale_impl" in doc["doc_health_reasons"] else " "
88
89
90 def render_text(report: DocReport) -> str:
91 """Return a terminal-friendly columnar view of *report*.
92
93 Columns: HEALTH ST KIND NAME FILE
94 Health bar + score, stale flag, symbol kind, name, file path.
95
96 Args:
97 report: The documentation report to render.
98 """
99 lines: list[str] = []
100 s = report["summary"]
101
102 lines.append(
103 f"Muse docs — commit {report['commit_id'][:8]} "
104 f"generated {report['generated_at'][:19]}"
105 )
106 lines.append(
107 f" symbols={s['total_symbols']} "
108 f"documented={s['documented']} "
109 f"undocumented={s['undocumented']} "
110 f"stale={s['stale_count']} "
111 f"avg_health={s['avg_health']:.2f} "
112 f"debt={s['doc_debt_score']:.2f}"
113 )
114 lines.append("")
115
116 if not report["symbols"]:
117 lines.append(" (no symbols)")
118 return "\n".join(lines)
119
120 header = f"{'HEALTH':17} ST {'KIND':14} {'NAME':38} FILE"
121 lines.append(header)
122 lines.append("-" * len(header))
123
124 for doc in report["symbols"]:
125 bar = _health_bar(doc["doc_health"])
126 score = f"{doc['doc_health']:.2f}"
127 health_col = f"{bar} {score}"
128 stale = _stale_flag(doc)
129 kind = doc["kind"][:14]
130 name = doc["name"][:38]
131 file_col = doc["file"]
132 lines.append(f"{health_col:17} {stale} {kind:14} {name:38} {file_col}")
133
134 if report["missing"]:
135 lines.append("")
136 lines.append(f"Missing docstrings ({len(report['missing'])} public symbols):")
137 for m in report["missing"][:20]:
138 lines.append(f" [{m['caller_count']:3} callers] {m['address']}")
139 if len(report["missing"]) > 20:
140 lines.append(f" … and {len(report['missing']) - 20} more")
141
142 if report["stale"]:
143 lines.append("")
144 lines.append(f"Potentially stale docstrings ({len(report['stale'])} symbols):")
145 for st in report["stale"][:10]:
146 lines.append(f" {st['address']}")
147 if len(report["stale"]) > 10:
148 lines.append(f" … and {len(report['stale']) - 10} more")
149
150 return "\n".join(lines)
151
152
153 # ---------------------------------------------------------------------------
154 # Markdown renderer
155 # ---------------------------------------------------------------------------
156
157 _MD_KIND_BADGE: _BadgeMap = {
158 "function": "fn",
159 "async_function": "async fn",
160 "class": "class",
161 "method": "method",
162 "async_method": "async method",
163 "variable": "var",
164 "import": "import",
165 "section": "section",
166 "rule": "rule",
167 }
168
169
170 def _md_health_indicator(score: float) -> str:
171 """Return an emoji indicator for the health score."""
172 if score >= 0.85:
173 return "✅"
174 if score >= 0.60:
175 return "🟡"
176 return "🔴"
177
178
179 def render_markdown(report: DocReport) -> str:
180 """Return GitHub-compatible Markdown for *report*.
181
182 Structure:
183 - H1 header with commit and summary stats
184 - Auto-generated TOC for all symbols
185 - Per-symbol H2 sections with docstring, signature, callers, callees,
186 version info, linked tests, and health score
187
188 Args:
189 report: The documentation report to render.
190 """
191 lines: list[str] = []
192 s = report["summary"]
193
194 lines.append("# Muse Documentation Report")
195 lines.append("")
196 lines.append(f"**Commit:** `{report['commit_id'][:8]}` ")
197 lines.append(f"**Generated:** {report['generated_at'][:19]} ")
198 lines.append(
199 f"**Health:** avg={s['avg_health']:.2f} "
200 f"debt={s['doc_debt_score']:.2f} "
201 f"documented={s['documented']}/{s['total_symbols']}"
202 )
203 lines.append("")
204
205 if report["symbols"]:
206 lines.append("## Table of Contents")
207 lines.append("")
208 for doc in report["symbols"]:
209 anchor = doc["address"].lower().replace(" ", "-").replace("/", "").replace("::", "--").replace(".", "")
210 indicator = _md_health_indicator(doc["doc_health"])
211 lines.append(f"- {indicator} [`{doc['qualified_name']}`](#{anchor})")
212 lines.append("")
213
214 for doc in report["symbols"]:
215 anchor = doc["address"].lower().replace(" ", "-").replace("/", "").replace("::", "--").replace(".", "")
216 kind_badge = _MD_KIND_BADGE.get(doc["kind"], doc["kind"])
217 indicator = _md_health_indicator(doc["doc_health"])
218
219 lines.append(f"## `{doc['qualified_name']}` {{#{anchor}}}")
220 lines.append("")
221 lines.append(
222 f"> {indicator} **{kind_badge}** in `{doc['file']}`"
223 f" (lines {doc['lineno']}–{doc['end_lineno']})"
224 )
225 lines.append("")
226
227 if doc["signature"]:
228 lines.append("```python")
229 lines.append(doc["signature"])
230 lines.append("```")
231 lines.append("")
232
233 if doc["docstring"]:
234 lines.append(doc["docstring"])
235 else:
236 lines.append("*No docstring.*")
237 lines.append("")
238
239 if doc["since_version"] or doc["since_commit"]:
240 ver = doc["since_version"] or ""
241 cid = (doc["since_commit"] or "")[:8]
242 lines.append(f"**Introduced:** {ver or cid}")
243 lines.append("")
244
245 if doc["last_changed_version"] or doc["last_changed_commit"]:
246 ver = doc["last_changed_version"] or ""
247 cid = (doc["last_changed_commit"] or "")[:8]
248 lines.append(f"**Last changed:** {ver or cid}")
249 lines.append("")
250
251 if doc["breaking_changes"]:
252 lines.append("**Breaking changes:**")
253 for bc in doc["breaking_changes"]:
254 lines.append(f"- {bc}")
255 lines.append("")
256
257 if doc["callers"]:
258 lines.append(f"**Called by** ({len(doc['callers'])}):")
259 for c in doc["callers"][:10]:
260 lines.append(f"- `{c}`")
261 if len(doc["callers"]) > 10:
262 lines.append(f"- *(+{len(doc['callers']) - 10} more)*")
263 lines.append("")
264
265 if doc["callees"]:
266 lines.append(f"**Calls** ({len(doc['callees'])}):")
267 for c in doc["callees"][:10]:
268 lines.append(f"- `{c}`")
269 if len(doc["callees"]) > 10:
270 lines.append(f"- *(+{len(doc['callees']) - 10} more)*")
271 lines.append("")
272
273 if doc["linked_tests"]:
274 lines.append(f"**Tests** ({len(doc['linked_tests'])}):")
275 for t in doc["linked_tests"][:5]:
276 lines.append(f"- `{t}`")
277 if len(doc["linked_tests"]) > 5:
278 lines.append(f"- *(+{len(doc['linked_tests']) - 5} more)*")
279 lines.append("")
280
281 reasons = doc["doc_health_reasons"]
282 lines.append(
283 f"**Doc health:** {doc['doc_health']:.2f}"
284 + (f" ({', '.join(reasons)})" if reasons else "")
285 )
286 lines.append("")
287 lines.append("---")
288 lines.append("")
289
290 if report["missing"]:
291 lines.append("## Missing Docstrings")
292 lines.append("")
293 lines.append(
294 f"{len(report['missing'])} public symbol(s) lack docstrings "
295 f"(sorted by caller count):"
296 )
297 lines.append("")
298 lines.append("| Callers | Address |")
299 lines.append("|---------|---------|")
300 for m in report["missing"]:
301 lines.append(f"| {m['caller_count']} | `{m['address']}` |")
302 lines.append("")
303
304 if report["stale"]:
305 lines.append("## Stale Docstrings")
306 lines.append("")
307 lines.append(
308 f"{len(report['stale'])} symbol(s) may have stale documentation:"
309 )
310 lines.append("")
311 for st in report["stale"]:
312 changed = "signature" if st["signature_changed"] else "body"
313 lines.append(f"- `{st['address']}` — {changed} changed")
314 lines.append("")
315
316 return "\n".join(lines)
317
318
319 # ---------------------------------------------------------------------------
320 # HTML renderer
321 # ---------------------------------------------------------------------------
322
323 _HTML_TEMPLATE = """\
324 <!DOCTYPE html>
325 <html lang="en">
326 <head>
327 <meta charset="utf-8">
328 <meta name="viewport" content="width=device-width, initial-scale=1">
329 <title>Muse Docs — {commit_short}</title>
330 <style>
331 :root {{
332 --bg: #0d1117; --surface: #161b22; --border: #30363d;
333 --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
334 --green: #3fb950; --yellow: #d29922; --red: #f85149;
335 --code-bg: #1f2428; --font: system-ui, -apple-system, sans-serif;
336 --mono: 'SFMono-Regular', Consolas, monospace;
337 }}
338 * {{ box-sizing: border-box; margin: 0; padding: 0; }}
339 body {{ background: var(--bg); color: var(--text); font-family: var(--font);
340 display: flex; height: 100vh; overflow: hidden; }}
341 nav {{ width: 280px; min-width: 220px; background: var(--surface);
342 border-right: 1px solid var(--border); overflow-y: auto;
343 padding: 16px 0; flex-shrink: 0; }}
344 nav h2 {{ font-size: 11px; text-transform: uppercase; letter-spacing: 1px;
345 color: var(--muted); padding: 0 16px 8px; }}
346 nav a {{ display: block; padding: 5px 16px; color: var(--muted);
347 text-decoration: none; font-size: 13px; white-space: nowrap;
348 overflow: hidden; text-overflow: ellipsis; }}
349 nav a:hover {{ color: var(--accent); background: rgba(88,166,255,.05); }}
350 main {{ flex: 1; overflow-y: auto; padding: 32px; }}
351 header {{ margin-bottom: 32px; }}
352 header h1 {{ font-size: 24px; margin-bottom: 8px; }}
353 .meta {{ color: var(--muted); font-size: 13px; }}
354 .stats {{ display: flex; gap: 24px; margin-top: 16px; flex-wrap: wrap; }}
355 .stat {{ background: var(--surface); border: 1px solid var(--border);
356 border-radius: 6px; padding: 12px 16px; }}
357 .stat-value {{ font-size: 22px; font-weight: 600; }}
358 .stat-label {{ font-size: 11px; color: var(--muted); text-transform: uppercase; }}
359 section.symbol {{ background: var(--surface); border: 1px solid var(--border);
360 border-radius: 8px; padding: 20px; margin-bottom: 20px; }}
361 section.symbol h2 {{ font-size: 16px; font-family: var(--mono); margin-bottom: 6px; }}
362 .badge {{ display: inline-block; padding: 2px 8px; border-radius: 4px;
363 font-size: 11px; font-weight: 600; text-transform: uppercase; }}
364 .badge-fn {{ background: #1f3a5f; color: var(--accent); }}
365 .badge-class {{ background: #2d1f5f; color: #d2a8ff; }}
366 .badge-method {{ background: #1a3a2a; color: var(--green); }}
367 .badge-var {{ background: #3a2a1a; color: #f0883e; }}
368 .badge-other {{ background: var(--border); color: var(--muted); }}
369 .health-bar {{ display: flex; align-items: center; gap: 8px; margin: 8px 0; }}
370 .health-track {{ flex: 1; max-width: 120px; height: 6px;
371 background: var(--border); border-radius: 3px; }}
372 .health-fill {{ height: 100%; border-radius: 3px; }}
373 .health-good {{ background: var(--green); }}
374 .health-mid {{ background: var(--yellow); }}
375 .health-bad {{ background: var(--red); }}
376 .health-score {{ font-size: 13px; color: var(--muted); }}
377 pre {{ background: var(--code-bg); border: 1px solid var(--border);
378 border-radius: 6px; padding: 12px; overflow-x: auto;
379 font-family: var(--mono); font-size: 13px; margin: 8px 0; }}
380 .docstring {{ background: var(--code-bg); border-left: 3px solid var(--accent);
381 padding: 12px; border-radius: 0 6px 6px 0; margin: 12px 0;
382 font-size: 14px; line-height: 1.6; white-space: pre-wrap; }}
383 .no-doc {{ color: var(--muted); font-style: italic; font-size: 13px; margin: 8px 0; }}
384 .section-title {{ font-size: 11px; text-transform: uppercase; color: var(--muted);
385 letter-spacing: 1px; margin: 12px 0 4px; }}
386 .pill-list {{ display: flex; flex-wrap: wrap; gap: 4px; }}
387 .pill {{ background: var(--code-bg); border: 1px solid var(--border);
388 border-radius: 4px; padding: 2px 8px; font-family: var(--mono);
389 font-size: 12px; color: var(--muted); }}
390 .version-badge {{ display: inline-block; background: #1a2a1a;
391 color: var(--green); border: 1px solid var(--green);
392 border-radius: 4px; padding: 2px 8px; font-size: 12px;
393 font-family: var(--mono); margin-right: 4px; }}
394 .stale-badge {{ background: #3a2a00; color: var(--yellow);
395 border: 1px solid var(--yellow); border-radius: 4px;
396 padding: 2px 8px; font-size: 11px; }}
397 .breaking {{ background: #3a1a1a; color: var(--red); border-radius: 4px;
398 padding: 4px 8px; font-size: 12px; margin: 2px 0; }}
399 .missing-table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
400 .missing-table th, .missing-table td {{ text-align: left; padding: 6px 12px;
401 border-bottom: 1px solid var(--border); }}
402 .missing-table th {{ color: var(--muted); font-size: 11px; text-transform: uppercase; }}
403 </style>
404 </head>
405 <body>
406 <nav>
407 <h2>Symbols</h2>
408 {nav_links}
409 </nav>
410 <main>
411 <header>
412 <h1>Muse Documentation</h1>
413 <div class="meta">Commit <code>{commit_short}</code> &nbsp;·&nbsp; {generated_at}</div>
414 <div class="stats">
415 <div class="stat">
416 <div class="stat-value">{total}</div>
417 <div class="stat-label">Symbols</div>
418 </div>
419 <div class="stat">
420 <div class="stat-value">{documented}</div>
421 <div class="stat-label">Documented</div>
422 </div>
423 <div class="stat">
424 <div class="stat-value">{undocumented}</div>
425 <div class="stat-label">Missing</div>
426 </div>
427 <div class="stat">
428 <div class="stat-value">{stale}</div>
429 <div class="stat-label">Stale</div>
430 </div>
431 <div class="stat">
432 <div class="stat-value">{avg_health}</div>
433 <div class="stat-label">Avg Health</div>
434 </div>
435 <div class="stat">
436 <div class="stat-value">{debt}</div>
437 <div class="stat-label">Doc Debt</div>
438 </div>
439 </div>
440 </header>
441 {symbol_sections}
442 {missing_section}
443 </main>
444 </body>
445 </html>
446 """
447
448 _BADGE_CLASS: _BadgeMap = {
449 "function": "badge-fn",
450 "async_function": "badge-fn",
451 "class": "badge-class",
452 "method": "badge-method",
453 "async_method": "badge-method",
454 "variable": "badge-var",
455 }
456
457
458 def _e(s: str) -> str:
459 """HTML-escape *s*."""
460 return _html_module.escape(s, quote=True)
461
462
463 def _health_class(score: float) -> str:
464 if score >= 0.75:
465 return "health-good"
466 if score >= 0.50:
467 return "health-mid"
468 return "health-bad"
469
470
471 def _render_symbol_html(doc: SymbolDoc) -> str:
472 anchor = _e(doc["address"].replace(" ", "_"))
473 kind_badge = _BADGE_CLASS.get(doc["kind"], "badge-other")
474 kind_label = _e(doc["kind"].replace("_", " "))
475 score_pct = int(doc["doc_health"] * 100)
476 health_cls = _health_class(doc["doc_health"])
477
478 parts: list[str] = []
479 parts.append(f'<section class="symbol" id="{anchor}">')
480 parts.append(
481 f' <h2><code>{_e(doc["qualified_name"])}</code>'
482 f' <span class="badge {kind_badge}">{kind_label}</span></h2>'
483 )
484 parts.append(
485 f' <div class="meta">{_e(doc["file"])} '
486 f' lines {doc["lineno"]}–{doc["end_lineno"]}</div>'
487 )
488
489 # Health bar
490 parts.append(' <div class="health-bar">')
491 parts.append(' <div class="health-track">')
492 parts.append(
493 f' <div class="health-fill {health_cls}" '
494 f'style="width:{score_pct}%"></div>'
495 )
496 parts.append(" </div>")
497 parts.append(f' <span class="health-score">{doc["doc_health"]:.2f}</span>')
498 if "stale_impl" in doc["doc_health_reasons"]:
499 parts.append(' <span class="stale-badge">⚠ stale</span>')
500 parts.append(" </div>")
501
502 # Version badges
503 if doc["since_version"]:
504 parts.append(
505 f' <span class="version-badge">since {_e(doc["since_version"])}</span>'
506 )
507 if doc["last_changed_version"]:
508 parts.append(
509 f' <span class="version-badge">changed {_e(doc["last_changed_version"])}</span>'
510 )
511
512 # Signature
513 if doc["signature"]:
514 parts.append(f" <pre>{_e(doc['signature'])}</pre>")
515
516 # Docstring
517 if doc["docstring"]:
518 parts.append(f' <div class="docstring">{_e(doc["docstring"])}</div>')
519 else:
520 parts.append(' <div class="no-doc">No docstring.</div>')
521
522 # Breaking changes
523 if doc["breaking_changes"]:
524 parts.append(' <div class="section-title">Breaking Changes</div>')
525 for bc in doc["breaking_changes"]:
526 parts.append(f' <div class="breaking">{_e(bc)}</div>')
527
528 # Callers
529 if doc["callers"]:
530 parts.append(
531 f' <div class="section-title">Called by ({len(doc["callers"])})</div>'
532 )
533 parts.append(' <div class="pill-list">')
534 for c in doc["callers"][:15]:
535 parts.append(f' <span class="pill">{_e(c)}</span>')
536 if len(doc["callers"]) > 15:
537 parts.append(
538 f' <span class="pill">+{len(doc["callers"]) - 15} more</span>'
539 )
540 parts.append(" </div>")
541
542 # Callees
543 if doc["callees"]:
544 parts.append(
545 f' <div class="section-title">Calls ({len(doc["callees"])})</div>'
546 )
547 parts.append(' <div class="pill-list">')
548 for c in doc["callees"][:15]:
549 parts.append(f' <span class="pill">{_e(c)}</span>')
550 if len(doc["callees"]) > 15:
551 parts.append(
552 f' <span class="pill">+{len(doc["callees"]) - 15} more</span>'
553 )
554 parts.append(" </div>")
555
556 # Linked tests
557 if doc["linked_tests"]:
558 parts.append(
559 f' <div class="section-title">Tests ({len(doc["linked_tests"])})</div>'
560 )
561 parts.append(' <div class="pill-list">')
562 for t in doc["linked_tests"][:8]:
563 parts.append(f' <span class="pill">{_e(t)}</span>')
564 if len(doc["linked_tests"]) > 8:
565 parts.append(
566 f' <span class="pill">+{len(doc["linked_tests"]) - 8} more</span>'
567 )
568 parts.append(" </div>")
569
570 parts.append("</section>")
571 return "\n".join(parts)
572
573
574 def render_html(report: DocReport) -> str:
575 """Return a self-contained HTML page for *report*.
576
577 The page includes an inline sidebar TOC, health bars, version badges,
578 caller/callee pill lists, and linked test lists. It has zero external
579 dependencies and opens in any browser.
580
581 Args:
582 report: The documentation report to render.
583 """
584 s = report["summary"]
585
586 nav_links_parts: list[str] = []
587 for doc in report["symbols"]:
588 anchor = _e(doc["address"].replace(" ", "_"))
589 indicator = "✅" if doc["doc_health"] >= 0.75 else ("🟡" if doc["doc_health"] >= 0.50 else "🔴")
590 nav_links_parts.append(
591 f' <a href="#{anchor}" title="{_e(doc["address"])}">'
592 f'{indicator} {_e(doc["name"])}</a>'
593 )
594
595 symbol_sections = "\n".join(
596 _render_symbol_html(doc) for doc in report["symbols"]
597 )
598
599 # Missing section
600 missing_parts: list[str] = []
601 if report["missing"]:
602 missing_parts.append(
603 f'<section class="symbol">'
604 f'<h2>Missing Docstrings ({len(report["missing"])})</h2>'
605 f'<table class="missing-table">'
606 f"<tr><th>Callers</th><th>Address</th><th>Kind</th></tr>"
607 )
608 for m in report["missing"]:
609 missing_parts.append(
610 f"<tr><td>{m['caller_count']}</td>"
611 f"<td><code>{_e(m['address'])}</code></td>"
612 f"<td>{_e(m['kind'])}</td></tr>"
613 )
614 missing_parts.append("</table></section>")
615
616 return _HTML_TEMPLATE.format(
617 commit_short=_e(report["commit_id"][:8]),
618 generated_at=_e(report["generated_at"][:19]),
619 total=s["total_symbols"],
620 documented=s["documented"],
621 undocumented=s["undocumented"],
622 stale=s["stale_count"],
623 avg_health=f"{s['avg_health']:.2f}",
624 debt=f"{s['doc_debt_score']:.2f}",
625 nav_links="\n".join(nav_links_parts),
626 symbol_sections=symbol_sections,
627 missing_section="\n".join(missing_parts),
628 )
629
630
631 # ---------------------------------------------------------------------------
632 # Dispatcher
633 # ---------------------------------------------------------------------------
634
635
636 def render(report: DocReport, fmt: RenderFormat) -> str:
637 """Render *report* in the requested *fmt*.
638
639 Args:
640 report: The documentation report from :func:`~doc_extractor.extract_docs`.
641 fmt: Output format: ``"json"``, ``"html"``, ``"markdown"``, or ``"text"``.
642
643 Returns:
644 A string in the requested format, ready to write to a file or stdout.
645
646 Raises:
647 ValueError: When *fmt* is not one of the supported values.
648 """
649 if fmt == "json":
650 return render_json(report)
651 if fmt == "html":
652 return render_html(report)
653 if fmt == "markdown":
654 return render_markdown(report)
655 if fmt == "text":
656 return render_text(report)
657 raise ValueError(f"Unsupported render format: {fmt!r}")
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 100 days ago