Coverage for src / tracekit / extensibility / docs.py: 72%

318 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-11 23:04 +0000

1"""Extension documentation auto-generation system. 

2 

3This module provides automatic documentation generation for TraceKit extensions 

4including API reference, usage examples, and metadata extraction from docstrings. 

5 

6 

7Example: 

8 >>> from tracekit.extensibility.docs import generate_extension_docs 

9 >>> from pathlib import Path 

10 >>> 

11 >>> # Generate documentation for an extension 

12 >>> docs = generate_extension_docs(Path("my_plugin/")) 

13 >>> print(docs.markdown) 

14""" 

15 

16from __future__ import annotations 

17 

18import ast 

19import inspect 

20import logging 

21from dataclasses import dataclass, field 

22from typing import TYPE_CHECKING, Any 

23 

24if TYPE_CHECKING: 

25 from pathlib import Path 

26 

27logger = logging.getLogger(__name__) 

28 

29 

30@dataclass 

31class FunctionDoc: 

32 """Documentation for a function or method. 

33 

34 Attributes: 

35 name: Function name 

36 signature: Full function signature 

37 docstring: Function docstring 

38 parameters: List of parameter descriptions 

39 returns: Return value description 

40 examples: Code examples from docstring 

41 """ 

42 

43 name: str 

44 signature: str = "" 

45 docstring: str = "" 

46 parameters: list[tuple[str, str]] = field(default_factory=list) 

47 returns: str = "" 

48 examples: list[str] = field(default_factory=list) 

49 

50 

51@dataclass 

52class ClassDoc: 

53 """Documentation for a class. 

54 

55 Attributes: 

56 name: Class name 

57 docstring: Class docstring 

58 methods: List of public method documentation 

59 attributes: List of class/instance attributes 

60 bases: List of base class names 

61 """ 

62 

63 name: str 

64 docstring: str = "" 

65 methods: list[FunctionDoc] = field(default_factory=list) 

66 attributes: list[tuple[str, str]] = field(default_factory=list) 

67 bases: list[str] = field(default_factory=list) 

68 

69 

70@dataclass 

71class ModuleDoc: 

72 """Documentation for a Python module. 

73 

74 Attributes: 

75 name: Module name 

76 docstring: Module docstring 

77 classes: List of class documentation 

78 functions: List of function documentation 

79 path: Source file path 

80 """ 

81 

82 name: str 

83 docstring: str = "" 

84 classes: list[ClassDoc] = field(default_factory=list) 

85 functions: list[FunctionDoc] = field(default_factory=list) 

86 path: str = "" 

87 

88 

89@dataclass 

90class ExtensionDocs: 

91 """Complete documentation for an extension. 

92 

93 Attributes: 

94 name: Extension name 

95 version: Extension version 

96 description: Extension description 

97 author: Extension author 

98 modules: List of module documentation 

99 metadata: Extension metadata 

100 markdown: Generated markdown documentation 

101 html: Generated HTML documentation 

102 """ 

103 

104 name: str 

105 version: str = "0.1.0" 

106 description: str = "" 

107 author: str = "" 

108 modules: list[ModuleDoc] = field(default_factory=list) 

109 metadata: dict[str, Any] = field(default_factory=dict) 

110 markdown: str = "" 

111 html: str = "" 

112 

113 

114def generate_extension_docs( 

115 extension_path: Path, 

116 *, 

117 include_private: bool = False, 

118 include_examples: bool = True, 

119 output_format: str = "markdown", 

120) -> ExtensionDocs: 

121 """Generate documentation for an extension. 

122 

123 Extracts documentation from Python modules, docstrings, and metadata files 

124 to create comprehensive API documentation. 

125 

126 Args: 

127 extension_path: Path to extension directory 

128 include_private: Include private members (starting with _) 

129 include_examples: Extract examples from docstrings 

130 output_format: Output format ("markdown" or "html") 

131 

132 Returns: 

133 ExtensionDocs object with generated documentation 

134 

135 Example: 

136 >>> from pathlib import Path 

137 >>> docs = generate_extension_docs(Path("plugins/my_decoder/")) 

138 >>> print(docs.markdown) 

139 >>> with open("docs/my_decoder.md", "w") as f: 

140 ... f.write(docs.markdown) 

141 

142 References: 

143 EXT-006: Extension Documentation 

144 """ 

145 docs = ExtensionDocs(name=extension_path.name) 

146 

147 # Extract metadata 

148 _extract_metadata(extension_path, docs) 

149 

150 # Document Python modules 

151 _document_modules(extension_path, docs, include_private, include_examples) 

152 

153 # Generate output 

154 if output_format == "markdown": 

155 docs.markdown = _generate_markdown(docs) 

156 elif output_format == "html": 156 ↛ 159line 156 didn't jump to line 159 because the condition on line 156 was always true

157 docs.html = _generate_html(docs) 

158 

159 return docs 

160 

161 

162def generate_decoder_docs( 

163 decoder_class: type, 

164 *, 

165 include_examples: bool = True, 

166) -> str: 

167 """Generate documentation for a decoder class. 

168 

169 Args: 

170 decoder_class: Decoder class to document 

171 include_examples: Include usage examples 

172 

173 Returns: 

174 Markdown documentation string 

175 

176 Example: 

177 >>> class MyDecoder: 

178 ... '''Custom UART decoder. 

179 ... 

180 ... Example: 

181 ... >>> decoder = MyDecoder() 

182 ... >>> frames = decoder.decode(signal) 

183 ... ''' 

184 ... def decode(self, signal): 

185 ... '''Decode signal.''' 

186 ... return [] 

187 >>> docs = generate_decoder_docs(MyDecoder) 

188 >>> print(docs) 

189 

190 References: 

191 EXT-006: Extension Documentation 

192 """ 

193 class_doc = _document_class( 

194 decoder_class, include_private=False, include_examples=include_examples 

195 ) 

196 

197 # Generate markdown 

198 lines = [] 

199 lines.append(f"# {class_doc.name}") 

200 lines.append("") 

201 

202 if class_doc.docstring: 202 ↛ 206line 202 didn't jump to line 206 because the condition on line 202 was always true

203 lines.append(class_doc.docstring) 

204 lines.append("") 

205 

206 if class_doc.bases: 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true

207 lines.append(f"**Inherits from:** {', '.join(class_doc.bases)}") 

208 lines.append("") 

209 

210 # Attributes 

211 if class_doc.attributes: 211 ↛ 212line 211 didn't jump to line 212 because the condition on line 211 was never true

212 lines.append("## Attributes") 

213 lines.append("") 

214 for name, desc in class_doc.attributes: 

215 lines.append(f"- **{name}**: {desc}") 

216 lines.append("") 

217 

218 # Methods 

219 if class_doc.methods: 219 ↛ 251line 219 didn't jump to line 251 because the condition on line 219 was always true

220 lines.append("## Methods") 

221 lines.append("") 

222 for method in class_doc.methods: 

223 lines.append(f"### {method.name}") 

224 lines.append("") 

225 if method.signature: 225 ↛ 230line 225 didn't jump to line 230 because the condition on line 225 was always true

226 lines.append("```python") 

227 lines.append(f"{method.signature}") 

228 lines.append("```") 

229 lines.append("") 

230 if method.docstring: 230 ↛ 233line 230 didn't jump to line 233 because the condition on line 230 was always true

231 lines.append(method.docstring) 

232 lines.append("") 

233 if method.parameters: 233 ↛ 234line 233 didn't jump to line 234 because the condition on line 233 was never true

234 lines.append("**Parameters:**") 

235 lines.append("") 

236 for param_name, param_desc in method.parameters: 

237 lines.append(f"- **{param_name}**: {param_desc}") 

238 lines.append("") 

239 if method.returns: 239 ↛ 240line 239 didn't jump to line 240 because the condition on line 239 was never true

240 lines.append(f"**Returns:** {method.returns}") 

241 lines.append("") 

242 if method.examples: 242 ↛ 243line 242 didn't jump to line 243 because the condition on line 242 was never true

243 lines.append("**Example:**") 

244 lines.append("") 

245 for example in method.examples: 

246 lines.append("```python") 

247 lines.append(example) 

248 lines.append("```") 

249 lines.append("") 

250 

251 return "\n".join(lines) 

252 

253 

254def extract_plugin_metadata( 

255 extension_path: Path, 

256) -> dict[str, Any]: 

257 """Extract metadata from extension directory. 

258 

259 Args: 

260 extension_path: Path to extension directory 

261 

262 Returns: 

263 Dictionary with metadata fields 

264 

265 Example: 

266 >>> metadata = extract_plugin_metadata(Path("plugins/my_plugin/")) 

267 >>> print(metadata["name"]) 

268 >>> print(metadata["version"]) 

269 

270 References: 

271 EXT-006: Extension Documentation 

272 """ 

273 metadata: dict[str, Any] = {} 

274 

275 # Try pyproject.toml 

276 pyproject = extension_path / "pyproject.toml" 

277 if pyproject.exists(): 

278 try: 

279 import tomllib 

280 

281 with open(pyproject, "rb") as f: 

282 data = tomllib.load(f) 

283 

284 if "project" in data: 284 ↛ 301line 284 didn't jump to line 301 because the condition on line 284 was always true

285 project = data["project"] 

286 metadata.update( 

287 { 

288 "name": project.get("name", ""), 

289 "version": project.get("version", ""), 

290 "description": project.get("description", ""), 

291 "authors": project.get("authors", []), 

292 "dependencies": project.get("dependencies", []), 

293 "entry_points": project.get("entry-points", {}), 

294 } 

295 ) 

296 

297 except Exception as e: 

298 logger.warning(f"Failed to parse pyproject.toml: {e}") 

299 

300 # Try plugin.yaml 

301 plugin_yaml = extension_path / "plugin.yaml" 

302 if plugin_yaml.exists(): 302 ↛ 303line 302 didn't jump to line 303 because the condition on line 302 was never true

303 try: 

304 import yaml 

305 

306 with open(plugin_yaml, encoding="utf-8") as f: 

307 data = yaml.safe_load(f) 

308 

309 if data: 

310 metadata.update(data) 

311 

312 except Exception as e: 

313 logger.warning(f"Failed to parse plugin.yaml: {e}") 

314 

315 return metadata 

316 

317 

318def _extract_metadata(extension_path: Path, docs: ExtensionDocs) -> None: 

319 """Extract metadata from extension files. 

320 

321 Args: 

322 extension_path: Path to extension directory 

323 docs: ExtensionDocs to populate 

324 """ 

325 metadata = extract_plugin_metadata(extension_path) 

326 

327 docs.name = metadata.get("name", extension_path.name) 

328 docs.version = metadata.get("version", "0.1.0") 

329 docs.description = metadata.get("description", "") 

330 docs.metadata = metadata 

331 

332 # Extract author 

333 authors = metadata.get("authors", []) 

334 if authors and isinstance(authors, list) and len(authors) > 0: 334 ↛ 335line 334 didn't jump to line 335 because the condition on line 334 was never true

335 if isinstance(authors[0], dict): 

336 docs.author = authors[0].get("name", "") 

337 else: 

338 docs.author = str(authors[0]) 

339 

340 

341def _document_modules( 

342 extension_path: Path, 

343 docs: ExtensionDocs, 

344 include_private: bool, 

345 include_examples: bool, 

346) -> None: 

347 """Document all Python modules in extension. 

348 

349 Args: 

350 extension_path: Path to extension directory 

351 docs: ExtensionDocs to populate 

352 include_private: Include private members 

353 include_examples: Extract examples from docstrings 

354 """ 

355 # Find Python files 

356 py_files = list(extension_path.glob("*.py")) 

357 py_files = [f for f in py_files if f.name != "__init__.py"] 

358 

359 for py_file in py_files: 

360 try: 

361 module_doc = _document_module(py_file, include_private, include_examples) 

362 docs.modules.append(module_doc) 

363 except Exception as e: 

364 logger.warning(f"Failed to document {py_file}: {e}") 

365 

366 

367def _document_module( 

368 module_path: Path, 

369 include_private: bool, 

370 include_examples: bool, 

371) -> ModuleDoc: 

372 """Document a Python module. 

373 

374 Args: 

375 module_path: Path to Python file 

376 include_private: Include private members 

377 include_examples: Extract examples 

378 

379 Returns: 

380 ModuleDoc with extracted documentation 

381 """ 

382 with open(module_path, encoding="utf-8") as f: 

383 source = f.read() 

384 

385 tree = ast.parse(source) 

386 

387 module_doc = ModuleDoc( 

388 name=module_path.stem, 

389 path=str(module_path), 

390 docstring=ast.get_docstring(tree) or "", 

391 ) 

392 

393 # Extract classes and functions 

394 for node in ast.iter_child_nodes(tree): 

395 if isinstance(node, ast.ClassDef): 

396 if include_private or not node.name.startswith("_"): 396 ↛ 394line 396 didn't jump to line 394 because the condition on line 396 was always true

397 class_doc = _document_class_ast(node, include_private, include_examples) 

398 module_doc.classes.append(class_doc) 

399 

400 elif isinstance(node, ast.FunctionDef): 

401 if include_private or not node.name.startswith("_"): 401 ↛ 394line 401 didn't jump to line 394 because the condition on line 401 was always true

402 func_doc = _document_function_ast(node, include_examples) 

403 module_doc.functions.append(func_doc) 

404 

405 return module_doc 

406 

407 

408def _document_class( 

409 cls: type, 

410 include_private: bool, 

411 include_examples: bool, 

412) -> ClassDoc: 

413 """Document a class from runtime object. 

414 

415 Args: 

416 cls: Class to document 

417 include_private: Include private members 

418 include_examples: Extract examples 

419 

420 Returns: 

421 ClassDoc with extracted documentation 

422 """ 

423 class_doc = ClassDoc( 

424 name=cls.__name__, 

425 docstring=inspect.getdoc(cls) or "", 

426 bases=[base.__name__ for base in cls.__bases__ if base is not object], 

427 ) 

428 

429 # Document methods 

430 for name, obj in inspect.getmembers(cls): 

431 if include_private or not name.startswith("_"): 

432 if inspect.isfunction(obj) or inspect.ismethod(obj): 432 ↛ 430line 432 didn't jump to line 430 because the condition on line 432 was always true

433 try: 

434 sig = str(inspect.signature(obj)) 

435 func_doc = FunctionDoc( 

436 name=name, 

437 signature=f"def {name}{sig}", 

438 docstring=inspect.getdoc(obj) or "", 

439 ) 

440 class_doc.methods.append(func_doc) 

441 except Exception: 

442 pass 

443 

444 return class_doc 

445 

446 

447def _document_class_ast( 

448 node: ast.ClassDef, 

449 include_private: bool, 

450 include_examples: bool, 

451) -> ClassDoc: 

452 """Document a class from AST node. 

453 

454 Args: 

455 node: AST ClassDef node 

456 include_private: Include private members 

457 include_examples: Extract examples 

458 

459 Returns: 

460 ClassDoc with extracted documentation 

461 """ 

462 class_doc = ClassDoc( 

463 name=node.name, 

464 docstring=ast.get_docstring(node) or "", 

465 bases=[_get_name_from_ast(base) for base in node.bases], 

466 ) 

467 

468 # Document methods 

469 for item in node.body: 

470 if isinstance(item, ast.FunctionDef): 

471 if include_private or not item.name.startswith("_"): 

472 func_doc = _document_function_ast(item, include_examples) 

473 class_doc.methods.append(func_doc) 

474 

475 return class_doc 

476 

477 

478def _document_function_ast( 

479 node: ast.FunctionDef, 

480 include_examples: bool, 

481) -> FunctionDoc: 

482 """Document a function from AST node. 

483 

484 Args: 

485 node: AST FunctionDef node 

486 include_examples: Extract examples 

487 

488 Returns: 

489 FunctionDoc with extracted documentation 

490 """ 

491 # Build signature 

492 args = [] 

493 for arg in node.args.args: 

494 args.append(arg.arg) 

495 

496 signature = f"def {node.name}({', '.join(args)})" 

497 

498 func_doc = FunctionDoc( 

499 name=node.name, 

500 signature=signature, 

501 docstring=ast.get_docstring(node) or "", 

502 ) 

503 

504 # Parse docstring for parameters, returns, examples 

505 if func_doc.docstring: 505 ↛ 508line 505 didn't jump to line 508 because the condition on line 505 was always true

506 _parse_docstring(func_doc, include_examples) 

507 

508 return func_doc 

509 

510 

511def _parse_docstring(func_doc: FunctionDoc, include_examples: bool) -> None: 

512 """Parse Google-style docstring for structured information. 

513 

514 Args: 

515 func_doc: FunctionDoc to populate 

516 include_examples: Extract examples 

517 """ 

518 lines = func_doc.docstring.split("\n") 

519 current_section = None 

520 section_content: list[str] = [] 

521 

522 for line in lines: 

523 line_stripped = line.strip() 

524 

525 # Detect sections 

526 if line_stripped.endswith(":") and line_stripped[:-1] in [ 

527 "Args", 

528 "Arguments", 

529 "Parameters", 

530 "Returns", 

531 "Return", 

532 "Example", 

533 "Examples", 

534 ]: 

535 # Process previous section 

536 if current_section: 

537 _process_section(func_doc, current_section, section_content, include_examples) 

538 

539 current_section = line_stripped[:-1].lower() 

540 section_content = [] 

541 else: 

542 section_content.append(line) 

543 

544 # Process final section 

545 if current_section: 

546 _process_section(func_doc, current_section, section_content, include_examples) 

547 

548 

549def _process_section( 

550 func_doc: FunctionDoc, 

551 section: str, 

552 content: list[str], 

553 include_examples: bool, 

554) -> None: 

555 """Process a docstring section. 

556 

557 Args: 

558 func_doc: FunctionDoc to populate 

559 section: Section name 

560 content: Section content lines 

561 include_examples: Extract examples 

562 """ 

563 if section in ["args", "arguments", "parameters"]: 

564 # Parse parameters 

565 for line in content: 

566 line = line.strip() 

567 if ":" in line: 

568 parts = line.split(":", 1) 

569 param_name = parts[0].strip() 

570 param_desc = parts[1].strip() 

571 func_doc.parameters.append((param_name, param_desc)) 

572 

573 elif section in ["returns", "return"]: 573 ↛ 576line 573 didn't jump to line 576 because the condition on line 573 was always true

574 func_doc.returns = "\n".join(content).strip() 

575 

576 elif section in ["example", "examples"] and include_examples: 

577 # Extract code blocks 

578 in_code = False 

579 code_lines = [] 

580 

581 for line in content: 

582 if ">>>" in line or "..." in line: 

583 in_code = True 

584 code_lines.append(line.strip()) 

585 elif in_code: 

586 if line.strip() and not line.strip().startswith("#"): 

587 if not (">>>" in line or "..." in line): 

588 in_code = False 

589 if code_lines: 

590 func_doc.examples.append("\n".join(code_lines)) 

591 code_lines = [] 

592 else: 

593 code_lines.append(line.strip()) 

594 

595 if code_lines: 

596 func_doc.examples.append("\n".join(code_lines)) 

597 

598 

599def _get_name_from_ast(node: ast.expr) -> str: 

600 """Extract name from AST expression. 

601 

602 Args: 

603 node: AST expression node 

604 

605 Returns: 

606 Name string 

607 """ 

608 if isinstance(node, ast.Name): 

609 return node.id 

610 elif isinstance(node, ast.Attribute): 

611 return node.attr 

612 else: 

613 return str(node) 

614 

615 

616def _generate_markdown(docs: ExtensionDocs) -> str: 

617 """Generate markdown documentation. 

618 

619 Args: 

620 docs: ExtensionDocs to render 

621 

622 Returns: 

623 Markdown string 

624 """ 

625 lines = [] 

626 

627 # Title 

628 lines.append(f"# {docs.name}") 

629 lines.append("") 

630 

631 # Metadata 

632 if docs.version: 632 ↛ 635line 632 didn't jump to line 635 because the condition on line 632 was always true

633 lines.append(f"**Version:** {docs.version}") 

634 lines.append("") 

635 if docs.author: 635 ↛ 636line 635 didn't jump to line 636 because the condition on line 635 was never true

636 lines.append(f"**Author:** {docs.author}") 

637 lines.append("") 

638 if docs.description: 638 ↛ 643line 638 didn't jump to line 643 because the condition on line 638 was always true

639 lines.append(docs.description) 

640 lines.append("") 

641 

642 # Dependencies 

643 if "dependencies" in docs.metadata: 643 ↛ 651line 643 didn't jump to line 651 because the condition on line 643 was always true

644 lines.append("## Dependencies") 

645 lines.append("") 

646 for dep in docs.metadata["dependencies"]: 646 ↛ 647line 646 didn't jump to line 647 because the loop on line 646 never started

647 lines.append(f"- {dep}") 

648 lines.append("") 

649 

650 # Modules 

651 for module in docs.modules: 

652 lines.append(f"## Module: {module.name}") 

653 lines.append("") 

654 if module.docstring: 654 ↛ 659line 654 didn't jump to line 659 because the condition on line 654 was always true

655 lines.append(module.docstring) 

656 lines.append("") 

657 

658 # Classes 

659 for cls in module.classes: 

660 lines.append(f"### Class: {cls.name}") 

661 lines.append("") 

662 if cls.docstring: 662 ↛ 667line 662 didn't jump to line 667 because the condition on line 662 was always true

663 lines.append(cls.docstring) 

664 lines.append("") 

665 

666 # Methods 

667 if cls.methods: 667 ↛ 659line 667 didn't jump to line 659 because the condition on line 667 was always true

668 lines.append("#### Methods") 

669 lines.append("") 

670 for method in cls.methods: 

671 lines.append(f"##### {method.name}") 

672 lines.append("") 

673 if method.signature: 673 ↛ 678line 673 didn't jump to line 678 because the condition on line 673 was always true

674 lines.append("```python") 

675 lines.append(method.signature) 

676 lines.append("```") 

677 lines.append("") 

678 if method.docstring: 678 ↛ 670line 678 didn't jump to line 670 because the condition on line 678 was always true

679 lines.append(method.docstring) 

680 lines.append("") 

681 

682 # Functions 

683 for func in module.functions: 

684 lines.append(f"### Function: {func.name}") 

685 lines.append("") 

686 if func.signature: 686 ↛ 691line 686 didn't jump to line 691 because the condition on line 686 was always true

687 lines.append("```python") 

688 lines.append(func.signature) 

689 lines.append("```") 

690 lines.append("") 

691 if func.docstring: 691 ↛ 683line 691 didn't jump to line 683 because the condition on line 691 was always true

692 lines.append(func.docstring) 

693 lines.append("") 

694 

695 return "\n".join(lines) 

696 

697 

698def _generate_html(docs: ExtensionDocs) -> str: 

699 """Generate HTML documentation. 

700 

701 Args: 

702 docs: ExtensionDocs to render 

703 

704 Returns: 

705 HTML string 

706 """ 

707 # Convert markdown to HTML (simple conversion) 

708 markdown = _generate_markdown(docs) 

709 

710 # Simple markdown-to-HTML conversion 

711 html_lines = ["<!DOCTYPE html>", "<html>", "<head>"] 

712 html_lines.append(f"<title>{docs.name} Documentation</title>") 

713 html_lines.append("<style>") 

714 html_lines.append("body { font-family: Arial, sans-serif; margin: 40px; }") 

715 html_lines.append("code { background: #f4f4f4; padding: 2px 4px; }") 

716 html_lines.append("pre { background: #f4f4f4; padding: 10px; }") 

717 html_lines.append("</style>") 

718 html_lines.append("</head>") 

719 html_lines.append("<body>") 

720 

721 # Very simple markdown-to-HTML 

722 for line in markdown.split("\n"): 

723 if line.startswith("# "): 

724 html_lines.append(f"<h1>{line[2:]}</h1>") 

725 elif line.startswith("## "): 

726 html_lines.append(f"<h2>{line[3:]}</h2>") 

727 elif line.startswith("### "): 727 ↛ 728line 727 didn't jump to line 728 because the condition on line 727 was never true

728 html_lines.append(f"<h3>{line[4:]}</h3>") 

729 elif line.startswith("```"): 729 ↛ 731line 729 didn't jump to line 731 because the condition on line 729 was never true

730 # Toggle code block 

731 if "<pre><code>" not in html_lines[-1] if html_lines else "": 

732 html_lines.append("<pre><code>") 

733 else: 

734 html_lines.append("</code></pre>") 

735 elif line.strip(): 

736 html_lines.append(f"<p>{line}</p>") 

737 

738 html_lines.append("</body>") 

739 html_lines.append("</html>") 

740 

741 return "\n".join(html_lines) 

742 

743 

744__all__ = [ 

745 "ClassDoc", 

746 "ExtensionDocs", 

747 "FunctionDoc", 

748 "ModuleDoc", 

749 "extract_plugin_metadata", 

750 "generate_decoder_docs", 

751 "generate_extension_docs", 

752]