Files
deusdata--codebase-memory-mcp/scripts/gen-py-stdlib.py
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

433 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3.9
"""
gen-py-stdlib.py — Generate cbm_python_stdlib_register from typeshed stubs.
Phase 10 of Python LSP integration. Walks a typeshed/stdlib checkout,
parses each .pyi via the standard `ast` module, and emits a single C
source file (internal/cbm/lsp/generated/python_stdlib_data.c) that
populates a CBMTypeRegistry with classes + their method names and
free functions per module.
This is a v1 implementation — overload stacks collapse to the first
signature, ParamSpec/TypeVarTuple/Concatenate are skipped, version
guards `if sys.version_info >= (X, Y):` are flattened (we take the
union of all branches). Per-symbol min/max version guards from the
plan are recorded as comments only in v1; py_lsp consumers should
treat the generator output as the 3.12 baseline.
Usage:
python3.9 scripts/gen-py-stdlib.py <typeshed-stdlib-path> <output.c>
Defaults to /tmp/python-lsp-references/typeshed/stdlib and
internal/cbm/lsp/generated/python_stdlib_data.c when run without args.
"""
from __future__ import annotations
import argparse
import ast
import dataclasses
import os
import sys
from pathlib import Path
from typing import Iterable
# v1 module allowlist — the plan's "top usage" set. Skip large / low-value
# modules (tkinter, turtle, curses, xml.*, email.*) to keep generated
# output manageable.
ALLOWED_MODULES = {
"builtins",
"typing",
"typing_extensions",
"os",
"sys",
"collections",
"functools",
"itertools",
"pathlib",
"json",
"re",
"dataclasses",
"enum",
"datetime",
"subprocess",
"logging",
"asyncio",
"unittest",
"argparse",
"contextlib",
"io",
"tempfile",
"shutil",
"inspect",
"abc",
"warnings",
"weakref",
"copy",
"pickle",
"time",
"math",
"string",
"socket",
"threading",
"multiprocessing",
"queue",
"urllib",
"http",
"concurrent",
}
@dataclasses.dataclass
class StubMethod:
name: str
@dataclasses.dataclass
class StubClass:
qualified_name: str
short_name: str
methods: list[str]
bases: list[str]
@dataclasses.dataclass
class StubFunction:
qualified_name: str
short_name: str
module_qn: str
@dataclasses.dataclass
class ModuleStubs:
module_qn: str
classes: list[StubClass]
functions: list[StubFunction]
def is_method(node: ast.AST) -> bool:
return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
def class_methods(class_node: ast.ClassDef) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for child in class_node.body:
if is_method(child):
name = child.name # type: ignore[attr-defined]
if not name.startswith("_") or name in {"__init__", "__call__", "__enter__", "__exit__"}:
if name not in seen:
seen.add(name)
out.append(name)
elif isinstance(child, ast.If):
# Flatten version guards: walk both branches.
for sub in child.body + child.orelse:
if is_method(sub):
name = sub.name # type: ignore[attr-defined]
if name not in seen:
seen.add(name)
out.append(name)
return out
def base_qns(class_node: ast.ClassDef, module_qn: str) -> list[str]:
out: list[str] = []
for base in class_node.bases:
if isinstance(base, ast.Name):
# Bare name — qualify within the same module by default.
out.append(f"{module_qn}.{base.id}")
elif isinstance(base, ast.Attribute):
# mod.SubClass
try:
text = ast.unparse(base) # type: ignore[attr-defined]
except Exception:
continue
out.append(text)
return out
@dataclasses.dataclass
class StarReExport:
target_module: str # e.g. "posixpath" / "ntpath"
def parse_module(path: Path, module_qn: str) -> tuple[ModuleStubs, list[StarReExport]]:
src = path.read_text(encoding="utf-8", errors="replace")
try:
tree = ast.parse(src)
except SyntaxError:
return ModuleStubs(module_qn=module_qn, classes=[], functions=[]), []
classes: list[StubClass] = []
functions: list[StubFunction] = []
star_imports: list[StarReExport] = []
seen_funcs: set[str] = set()
def walk(body: list[ast.stmt], current_module_qn: str) -> None:
for node in body:
if isinstance(node, ast.ClassDef):
qn = f"{current_module_qn}.{node.name}"
classes.append(StubClass(
qualified_name=qn,
short_name=node.name,
methods=class_methods(node),
bases=base_qns(node, current_module_qn),
))
elif is_method(node):
name = node.name # type: ignore[attr-defined]
if name.startswith("_") and name not in {"__init__"}:
continue
if name in seen_funcs:
continue
seen_funcs.add(name)
functions.append(StubFunction(
qualified_name=f"{current_module_qn}.{name}",
short_name=name,
module_qn=current_module_qn,
))
elif isinstance(node, ast.ImportFrom):
# Track `from X import *` for re-export resolution.
if node.module and any(alias.name == "*" for alias in node.names):
star_imports.append(StarReExport(target_module=node.module))
elif isinstance(node, ast.If):
# Flatten version guards.
walk(node.body, current_module_qn)
walk(node.orelse, current_module_qn)
walk(tree.body, module_qn)
return ModuleStubs(module_qn=module_qn, classes=classes, functions=functions), star_imports
def collect_all_stubs(stdlib_root: Path) -> tuple[dict[str, ModuleStubs], dict[str, list[StarReExport]]]:
"""Gather every stub in the allowlist plus any re-export targets they
pull in transitively (e.g. os.path -> posixpath -> genericpath).
Returns (modules, star_imports_per_module). """
modules: dict[str, ModuleStubs] = {}
star_imports: dict[str, list[StarReExport]] = {}
# First pass: walk allowlist
queue: list[tuple[Path, str]] = []
for path in sorted(stdlib_root.rglob("*.pyi")):
rel = path.relative_to(stdlib_root)
parts = list(rel.parts)
if parts[-1] == "__init__.pyi":
parts.pop()
else:
parts[-1] = parts[-1][:-len(".pyi")]
if not parts:
continue
top = parts[0]
if top.startswith("_"):
continue
if top not in ALLOWED_MODULES:
continue
module_qn = ".".join(parts)
queue.append((path, module_qn))
seen_targets: set[str] = set()
while queue:
path, mod_qn = queue.pop()
if mod_qn in modules:
continue
ms, stars = parse_module(path, mod_qn)
modules[mod_qn] = ms
star_imports[mod_qn] = stars
# Enqueue re-export targets (e.g. posixpath, ntpath, genericpath)
for star in stars:
tgt = star.target_module
if tgt in seen_targets:
continue
seen_targets.add(tgt)
tgt_path = stdlib_root / (tgt.replace(".", "/") + ".pyi")
if tgt_path.is_file():
queue.append((tgt_path, tgt))
else:
tgt_init = stdlib_root / tgt.replace(".", "/") / "__init__.pyi"
if tgt_init.is_file():
queue.append((tgt_init, tgt))
return modules, star_imports
def resolve_reexports(modules: dict[str, ModuleStubs],
star_imports: dict[str, list[StarReExport]]) -> None:
"""For each module with `from X import *`, copy X's classes/functions
into the current module under that module's QN. Iterates to a fixed
point to handle re-export chains. """
changed = True
iter_count = 0
while changed and iter_count < 8:
changed = False
iter_count += 1
for mod_qn, stars in star_imports.items():
ms = modules[mod_qn]
for star in stars:
target = modules.get(star.target_module)
if not target:
continue
# Copy target's classes/functions under mod_qn
existing_class_names = {c.short_name for c in ms.classes}
existing_func_names = {f.short_name for f in ms.functions}
for c in target.classes:
if c.short_name in existing_class_names:
continue
ms.classes.append(StubClass(
qualified_name=f"{mod_qn}.{c.short_name}",
short_name=c.short_name,
methods=list(c.methods),
bases=list(c.bases),
))
existing_class_names.add(c.short_name)
changed = True
for f in target.functions:
if f.short_name in existing_func_names:
continue
ms.functions.append(StubFunction(
qualified_name=f"{mod_qn}.{f.short_name}",
short_name=f.short_name,
module_qn=mod_qn,
))
existing_func_names.add(f.short_name)
changed = True
def iter_module_stubs(stdlib_root: Path) -> Iterable[ModuleStubs]:
modules, star_imports = collect_all_stubs(stdlib_root)
resolve_reexports(modules, star_imports)
for mod_qn in sorted(modules.keys()):
yield modules[mod_qn]
C_HEADER = """\
// AUTO-GENERATED by scripts/gen-py-stdlib.py — DO NOT EDIT
// Python stdlib type information for LSP type resolver.
//
// Source: typeshed commit a7912d521e16ff63caf7a8b64b9072542be36777
// Module allowlist: see ALLOWED_MODULES in scripts/gen-py-stdlib.py
//
// v1 simplifications (per PYTHON_LSP_PLAN.md Phase 10):
// - overload stacks collapse to first signature
// - ParamSpec / TypeVarTuple / Concatenate skipped
// - version guards flattened (union of all branches)
// - per-symbol min/max version guards not yet emitted
#include "../type_rep.h"
#include "../type_registry.h"
#include <string.h>
#define CBM_PYTHON_STDLIB_GENERATED 1
void cbm_python_stdlib_register(CBMTypeRegistry* reg, CBMArena* arena) {
if (!reg || !arena) return;
CBMRegisteredType rt;
CBMRegisteredFunc rf;
"""
C_FOOTER = """\
}
"""
def c_escape(s: str) -> str:
return s.replace("\\", "\\\\").replace("\"", "\\\"")
def emit(stubs: list[ModuleStubs], output_path: Path) -> None:
method_table_id = 0
out_lines: list[str] = []
for ms in stubs:
if not ms.classes and not ms.functions:
continue
out_lines.append(f"\n /* ===== module: {ms.module_qn} ===== */\n")
for cls in ms.classes:
method_array_name = None
if cls.methods:
method_table_id += 1
method_array_name = f"py_methods_{method_table_id}"
methods_c = ", ".join(f"\"{c_escape(m)}\"" for m in cls.methods)
out_lines.append(
f" static const char* {method_array_name}[] = {{ {methods_c}, NULL }};\n"
)
out_lines.append(" memset(&rt, 0, sizeof(rt));\n")
out_lines.append(
f" rt.qualified_name = \"{c_escape(cls.qualified_name)}\";\n"
)
out_lines.append(
f" rt.short_name = \"{c_escape(cls.short_name)}\";\n"
)
if method_array_name:
out_lines.append(f" rt.method_names = {method_array_name};\n")
if cls.bases:
method_table_id += 1
bases_array = f"py_bases_{method_table_id}"
bases_c = ", ".join(f"\"{c_escape(b)}\"" for b in cls.bases)
out_lines.append(
f" static const char* {bases_array}[] = {{ {bases_c}, NULL }};\n"
)
out_lines.append(f" rt.embedded_types = {bases_array};\n")
out_lines.append(" cbm_registry_add_type(reg, rt);\n")
# Also emit a registered method per class.method so registry
# lookup_method(class_qn, name) works.
for m in cls.methods:
out_lines.append(" memset(&rf, 0, sizeof(rf));\n")
out_lines.append(
f" rf.qualified_name = \"{c_escape(cls.qualified_name)}.{c_escape(m)}\";\n"
)
out_lines.append(f" rf.short_name = \"{c_escape(m)}\";\n")
out_lines.append(
f" rf.receiver_type = \"{c_escape(cls.qualified_name)}\";\n"
)
out_lines.append(" cbm_registry_add_func(reg, rf);\n")
for fn in ms.functions:
out_lines.append(" memset(&rf, 0, sizeof(rf));\n")
out_lines.append(
f" rf.qualified_name = \"{c_escape(fn.qualified_name)}\";\n"
)
out_lines.append(f" rf.short_name = \"{c_escape(fn.short_name)}\";\n")
out_lines.append(" cbm_registry_add_func(reg, rf);\n")
output_path.write_text(C_HEADER + "".join(out_lines) + C_FOOTER, encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"stdlib_root",
nargs="?",
default="/tmp/python-lsp-references/typeshed/stdlib",
)
parser.add_argument(
"output",
nargs="?",
default="internal/cbm/lsp/generated/python_stdlib_data.c",
)
args = parser.parse_args()
root = Path(args.stdlib_root).resolve()
if not root.is_dir():
print(f"error: {root} is not a directory", file=sys.stderr)
return 1
stubs = list(iter_module_stubs(root))
out_path = Path(args.output).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
emit(stubs, out_path)
n_classes = sum(len(s.classes) for s in stubs)
n_methods = sum(sum(len(c.methods) for c in s.classes) for s in stubs)
n_funcs = sum(len(s.functions) for s in stubs)
print(
f"wrote {out_path}: {len(stubs)} modules, "
f"{n_classes} classes ({n_methods} methods), {n_funcs} free functions"
)
return 0
if __name__ == "__main__":
sys.exit(main())