| |
| |
| """ |
| MkDocs hook to automatically convert inline code references to API doc links. |
| |
| For example, `WeightTransferConfig` becomes |
| [`WeightTransferConfig`][vllm.config.WeightTransferConfig] |
| |
| This works with the `autorefs` plugin to create clickable cross-references |
| to API documentation pages generated by `mkdocstrings`. |
| |
| The hook builds an index of all documented public Python names (classes and |
| functions with docstrings) from the vllm package at startup using AST parsing, |
| then substitutes matching inline code spans on each page. Names without |
| docstrings are excluded because mkdocstrings will not generate a page for them. |
| """ |
|
|
| import ast |
| import logging |
| from pathlib import Path |
|
|
| import regex as re |
| from mkdocs.config.defaults import MkDocsConfig |
| from mkdocs.structure.files import Files |
| from mkdocs.structure.pages import Page |
|
|
| logger = logging.getLogger("mkdocs") |
|
|
| ROOT_DIR = Path(__file__).parent.parent.parent.parent.resolve() |
| VLLM_DIR = ROOT_DIR / "vllm" |
|
|
| |
| _name_index: dict[str, str] = {} |
|
|
| |
| _FENCED_BLOCK = re.compile( |
| r"(?:^|\n)(?P<fence>`{3,}|~{3,})[^\n]*\n.*?(?:\n(?P=fence))", re.DOTALL |
| ) |
|
|
| |
| |
| _INLINE_CODE = re.compile( |
| r"(?<!\[)" |
| r"`(?P<name>[A-Za-z0-9_]*)`" |
| r"(?!\])" |
| ) |
|
|
|
|
| def _has_docstring(node: ast.AST) -> bool: |
| """Check if a class or function node has a docstring.""" |
| if not isinstance(node, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef): |
| return False |
| return ast.get_docstring(node, clean=False) is not None |
|
|
|
|
| def _module_path(filepath: Path) -> str: |
| """Convert a filesystem path to a dotted module path.""" |
| rel = filepath.relative_to(ROOT_DIR) |
| parts = list(rel.with_suffix("").parts) |
| if parts[-1] == "__init__": |
| parts = parts[:-1] |
| return ".".join(parts) |
|
|
|
|
| def _index_file(filepath: Path) -> dict[str, str]: |
| """Extract documented public names from a Python file using AST parsing. |
| |
| Only classes and functions with docstrings are included, since |
| mkdocstrings won't generate a page for undocumented symbols. |
| """ |
| names: dict[str, str] = {} |
| try: |
| source = filepath.read_text(encoding="utf-8") |
| tree = ast.parse(source, filename=str(filepath)) |
| except (SyntaxError, UnicodeDecodeError): |
| return names |
|
|
| module = _module_path(filepath) |
|
|
| for node in ast.iter_child_nodes(tree): |
| if ( |
| |
| isinstance(node, ast.ClassDef) |
| and not node.name.startswith("_") |
| and _has_docstring(node) |
| ) or ( |
| |
| isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) |
| and not node.name.startswith("_") |
| and node.name[0].isupper() |
| and _has_docstring(node) |
| ): |
| names[node.name] = f"{module}.{node.name}" |
|
|
| return names |
|
|
|
|
| def _build_index() -> dict[str, str]: |
| """Walk the vllm package and build a name -> qualified path index.""" |
| index: dict[str, str] = {} |
| |
| |
| depth: dict[str, int] = {} |
|
|
| for filepath in sorted(VLLM_DIR.rglob("*.py")): |
| |
| if any(part.startswith("_") and part != "__init__" for part in filepath.parts): |
| continue |
| |
| rel = filepath.relative_to(VLLM_DIR) |
| if rel.parts and rel.parts[0] in ("third_party", "vllm_flash_attn"): |
| continue |
|
|
| module_depth = len(filepath.relative_to(ROOT_DIR).parts) |
| file_names = _index_file(filepath) |
|
|
| for name, qualified in file_names.items(): |
| if name not in index or module_depth < depth[name]: |
| index[name] = qualified |
| depth[name] = module_depth |
|
|
| return index |
|
|
|
|
| def on_startup(*, command: str, dirty: bool) -> None: |
| """Build the name index once at startup.""" |
| global _name_index |
| _name_index = _build_index() |
| logger.info("autoref_code: indexed %d names from vllm/", len(_name_index)) |
|
|
|
|
| def on_page_markdown( |
| markdown: str, *, page: Page, config: MkDocsConfig, files: Files |
| ) -> str: |
| """Replace inline code references with autoref links.""" |
| if not _name_index: |
| return markdown |
|
|
| |
| if page.file.src_path.startswith("api/"): |
| return markdown |
|
|
| |
| masks: list[str] = [] |
|
|
| def _mask_block(match: re.Match) -> str: |
| masks.append(match.group(0)) |
| return f"\ue000CODEBLOCK{len(masks) - 1}\ue000" |
|
|
| masked = _FENCED_BLOCK.sub(_mask_block, markdown) |
|
|
| |
| def _replace(match: re.Match) -> str: |
| name = match.group("name") |
| qualified = _name_index.get(name) |
| if qualified is None: |
| return match.group(0) |
| logger.debug("autoref_code: linking `%s` to [%s]", name, qualified) |
| return f"[`{name}`][{qualified}]" |
|
|
| result = _INLINE_CODE.sub(_replace, masked) |
|
|
| |
| result = re.sub( |
| r"\ue000CODEBLOCK(\d+)\ue000", lambda m: masks[int(m.group(1))], result |
| ) |
| return result |
|
|