Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / Resolve release version (push) Waiting to run
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Blocked by required conditions
CI: cua-driver distro-compat matrix / Distro compat summary (push) Blocked by required conditions
CI: Nix Linux Rust source / Nix / compositor build (push) Waiting to run
CI: Nix Linux Rust source / Nix / driver package (push) Waiting to run
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Waiting to run
CI: Rust Linux unit / Rust Linux unit and compile (push) Waiting to run
CI: Rust Windows unit / Rust Windows unit and compile (push) Waiting to run
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Waiting to run
CD: Docs MCP Server / build (linux/amd64) (push) Waiting to run
CD: Docs MCP Server / build (linux/arm64) (push) Waiting to run
CD: Docs MCP Server / merge (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

310 lines
9.6 KiB
Python

#!/usr/bin/env python3
"""
Extract Python API documentation using griffe.
This script extracts structured documentation from Python packages
without requiring them to be installed or imported.
Usage:
python extract_python_docs.py <package_path> <package_name>
Example:
python extract_python_docs.py libs/python/computer/computer computer
"""
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from griffe import (
Alias,
Attribute,
Class,
DocstringSectionKind,
Function,
Module,
Object,
load,
)
def extract_docstring_sections(obj: Object) -> Dict[str, Any]:
"""Extract parsed docstring sections from an object."""
result = {
"description": "",
"params": [],
"returns": None,
"raises": [],
"examples": [],
}
if not obj.docstring:
return result
# Get parsed sections (griffe parses automatically based on detected style)
try:
sections = obj.docstring.parsed
except Exception:
# Fallback to raw docstring
result["description"] = str(obj.docstring.value) if obj.docstring else ""
return result
for section in sections:
if section.kind == DocstringSectionKind.text:
result["description"] = str(section.value).strip()
elif section.kind == DocstringSectionKind.parameters:
for param in section.value:
result["params"].append(
{
"name": param.name,
"type": str(param.annotation) if param.annotation else "",
"description": param.description or "",
"default": str(param.default) if param.default else None,
}
)
elif section.kind == DocstringSectionKind.returns:
ret = section.value
if ret:
result["returns"] = {
"type": (
str(ret.annotation) if hasattr(ret, "annotation") and ret.annotation else ""
),
"description": ret.description if hasattr(ret, "description") else str(ret),
}
elif section.kind == DocstringSectionKind.raises:
for exc in section.value:
result["raises"].append(
{
"type": str(exc.annotation) if exc.annotation else "",
"description": exc.description or "",
}
)
elif section.kind == DocstringSectionKind.examples:
result["examples"].append(str(section.value))
return result
def extract_function(fn: Function, is_method: bool = False) -> Dict[str, Any]:
"""Extract function/method documentation."""
# Check if async (stored in labels)
is_async = "async" in getattr(fn, "labels", set())
# Build signature
params = []
for param in fn.parameters:
param_str = param.name
if param.annotation:
param_str += f": {param.annotation}"
if param.default is not None:
param_str += f" = {param.default}"
params.append(param_str)
signature = f"{'async ' if is_async else ''}def {fn.name}({', '.join(params)})"
if fn.returns:
signature += f" -> {fn.returns}"
docstring_data = extract_docstring_sections(fn)
return {
"name": fn.name,
"signature": signature,
"is_async": is_async,
"is_method": is_method,
"description": docstring_data["description"],
"parameters": docstring_data["params"],
"returns": docstring_data["returns"],
"raises": docstring_data["raises"],
"examples": docstring_data["examples"],
"is_private": fn.name.startswith("_") and not fn.name.startswith("__"),
"is_dunder": fn.name.startswith("__") and fn.name.endswith("__"),
}
def extract_attribute(attr: Attribute) -> Dict[str, Any]:
"""Extract attribute/property documentation."""
return {
"name": attr.name,
"type": str(attr.annotation) if attr.annotation else "",
"description": str(attr.docstring.value) if attr.docstring else "",
"default": str(attr.value) if attr.value else None,
"is_private": attr.name.startswith("_"),
}
def resolve_member(member: Any) -> Any:
"""Resolve an alias to its target if needed."""
if isinstance(member, Alias):
try:
return member.target
except Exception:
return member
return member
def extract_class(cls: Class) -> Dict[str, Any]:
"""Extract class documentation."""
docstring_data = extract_docstring_sections(cls)
# Extract methods
methods = []
for name, member in cls.members.items():
member = resolve_member(member)
if isinstance(member, Function):
method_doc = extract_function(member, is_method=True)
# Skip private methods except __init__
if not method_doc["is_private"] or name == "__init__":
methods.append(method_doc)
# Extract attributes/properties
attributes = []
for name, member in cls.members.items():
member = resolve_member(member)
if isinstance(member, Attribute) and not name.startswith("_"):
attributes.append(extract_attribute(member))
# Get base classes
bases = []
for base in cls.bases:
bases.append(str(base))
return {
"name": cls.name,
"description": docstring_data["description"],
"bases": bases,
"methods": methods,
"attributes": attributes,
"is_private": cls.name.startswith("_"),
}
def extract_module(module: Module, include_private: bool = False) -> Dict[str, Any]:
"""Extract module documentation."""
# Get version from module if available
version = "unknown"
if "__version__" in module.members:
version_attr = module.members["__version__"]
if hasattr(version_attr, "value") and version_attr.value:
version = str(version_attr.value).strip("'\"")
# Get __all__ exports if defined
exports = None
if "__all__" in module.members:
all_attr = module.members["__all__"]
if hasattr(all_attr, "value") and all_attr.value:
try:
# Try to parse __all__ as a list
exports = eval(str(all_attr.value))
except Exception:
exports = None
# Extract classes
classes = []
for name, member in module.members.items():
member = resolve_member(member)
if isinstance(member, Class):
# Include if in __all__ or if public
if exports is None:
if not name.startswith("_") or include_private:
classes.append(extract_class(member))
elif name in exports:
classes.append(extract_class(member))
# Extract module-level functions
functions = []
for name, member in module.members.items():
member = resolve_member(member)
if isinstance(member, Function):
if exports is None:
if not name.startswith("_") or include_private:
functions.append(extract_function(member))
elif name in exports:
functions.append(extract_function(member))
return {
"name": module.name,
"version": version,
"docstring": str(module.docstring.value) if module.docstring else "",
"exports": exports,
"classes": classes,
"functions": functions,
}
def extract_package_docs(package_path: str, package_name: str) -> Dict[str, Any]:
"""
Extract documentation from a Python package.
Args:
package_path: Path to the package directory (e.g., 'libs/python/computer/computer')
package_name: Name of the package to load (e.g., 'computer')
Returns:
Dictionary containing structured documentation
"""
# Resolve paths
package_dir = Path(package_path).resolve()
search_path = package_dir.parent
# Load the package using griffe
try:
package = load(
package_name,
search_paths=[str(search_path)],
)
except Exception as e:
return {
"error": f"Failed to load package: {e}",
"name": package_name,
"version": "unknown",
"modules": [],
}
# Extract main module
main_module = extract_module(package)
# Extract submodules
submodules = []
for name, member in package.members.items():
member = resolve_member(member)
if isinstance(member, Module) and not name.startswith("_"):
submodule_doc = extract_module(member)
if submodule_doc["classes"] or submodule_doc["functions"]:
submodules.append(submodule_doc)
return {
"name": package_name,
"version": main_module["version"],
"docstring": main_module["docstring"],
"exports": main_module["exports"],
"classes": main_module["classes"],
"functions": main_module["functions"],
"submodules": submodules,
}
def main():
"""Main entry point."""
if len(sys.argv) < 3:
print("Usage: python extract_python_docs.py <package_path> <package_name>", file=sys.stderr)
print(
"Example: python extract_python_docs.py libs/python/computer/computer computer",
file=sys.stderr,
)
sys.exit(1)
package_path = sys.argv[1]
package_name = sys.argv[2]
docs = extract_package_docs(package_path, package_name)
print(json.dumps(docs, indent=2))
if __name__ == "__main__":
main()