.pydev-tools / _ast / reference.py
.pydev-tools / _ast / reference.py
#!/usr/bin/env python3
"""Generate an external API reference (Markdown) from type hints + docstrings.
Walks a source tree and emits a Markdown document listing classes, functions and
module-level variables with their signatures, parameters, return types and
docstrings — no manual drafting required. Emits JSON with the report path.
Usage: reference.py <target_path> <output_path> [max_length]
"""
import ast, json, os, sys, textwrap
def load_py_files(path):
files = []
if os.path.isfile(path):
files.append(path)
elif os.path.isdir(path):
for root, _dirs, names in os.walk(path):
for fn in sorted(names):
if fn.endswith(".py"):
files.append(os.path.join(root, fn))
return sorted(files)
def annotation_str(node):
if node is None:
return "Any"
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
parts = []
cur = node
while isinstance(cur, ast.Attribute):
parts.append(cur.attr)
cur = cur.value
if isinstance(cur, ast.Name):
parts.append(cur.id)
return ".".join(reversed(parts))
if isinstance(node, ast.Subscript):
base = annotation_str(node.value)
idx = annotation_str(node.slice) if node.slice is not None else "?"
return f"{base}[{idx}]"
if isinstance(node, ast.Tuple):
parts = [annotation_str(e) for e in node.elts]
return "(" + ", ".join(parts) + ")"
if isinstance(node, ast.List):
parts = [annotation_str(e) for e in node.elts]
return "[" + ", ".join(parts) + "]"
if isinstance(node, (ast.Constant,)):
return repr(node.value)
if isinstance(node, ast.Starred):
return "*" + annotation_str(node.value)
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
return "|".join([annotation_str(node.left), annotation_str(node.right)])
return "Any"
def shorten(text, max_length):
text = (text or "").strip()
if len(text) <= max_length:
return text
if "\n" in text:
first, _, rest = text.partition("\n")
first = first.strip()
if len(first) <= max_length:
return first + "\n\n" + shorten(rest, max_length)
return text[:max_length - 1] + "…"
def collect(tree, out):
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
args = [a.arg for a in node.args.args if a.arg != "self"]
kwonly = [a.arg for a in node.args.kwonlyargs]
params = ", ".join(args + kwonly) or "(no params)"
ret = annotation_str(node.returns) if node.returns else "None"
doc = shorten(ast.get_docstring(node), 500)
out.append({"kind": "function", "name": node.name, "params": params, "return": ret, "doc": doc})
elif isinstance(node, ast.ClassDef):
doc = shorten(ast.get_docstring(node), 500)
methods = ", ".join(sorted(m.name for m in node.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))))
out.append({"kind": "class", "name": node.name, "methods": methods, "doc": doc})
elif isinstance(node, ast.Assign):
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
try:
doc = shorten(ast.get_docstring(node), 300)
except TypeError:
doc = ""
out.append({"kind": "variable", "name": node.targets[0].id, "value": annotation_str(node.value), "doc": doc})
def main():
files = load_py_files(sys.argv[1])
max_length = int(sys.argv[3]) if len(sys.argv) > 3 else 500
entries = []
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
except (SyntaxError, OSError):
continue
collect(tree, entries)
functions = [e for e in entries if e["kind"] == "function"]
classes = [e for e in entries if e["kind"] == "class"]
variables = [e for e in entries if e["kind"] == "variable"]
def section(title, items):
if not items:
return ""
md = ["## " + title, ""]
for it in items:
if it["kind"] == "class":
md.append(f"### `{it['name']}`")
md.append("")
md.append(it.get("doc", ""))
md.append("")
if it.get("methods"):
md.append("Methods: " + it["methods"])
else:
md.append(f"- **{it['name']}**({', '.join([p for p in [it.get('params')] if p])}) -> {it.get('return', 'None') or 'None'}")
md.append(it.get("doc", ""))
return "\n".join(md)
report = [
"# API Reference",
"",
f"Auto-generated from `{sys.argv[1]}`.",
"",
section("Classes", classes),
section("Functions", functions),
section("Module Variables", variables),
]
out_path = sys.argv[2].strip()
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(report))
json.dump({"outputPath": out_path, "entriesCount": len(functions) + len(classes) + len(variables), "bytesWritten": len("\n".join(report))}, sys.stdout, indent=2)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Generate an external API reference (Markdown) from type hints + docstrings.
Walks a source tree and emits a Markdown document listing classes, functions and
module-level variables with their signatures, parameters, return types and
docstrings — no manual drafting required. Emits JSON with the report path.
Usage: reference.py <target_path> <output_path> [max_length]
"""
import ast, json, os, sys, textwrap
def load_py_files(path):
files = []
if os.path.isfile(path):
files.append(path)
elif os.path.isdir(path):
for root, _dirs, names in os.walk(path):
for fn in sorted(names):
if fn.endswith(".py"):
files.append(os.path.join(root, fn))
return sorted(files)
def annotation_str(node):
if node is None:
return "Any"
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
parts = []
cur = node
while isinstance(cur, ast.Attribute):
parts.append(cur.attr)
cur = cur.value
if isinstance(cur, ast.Name):
parts.append(cur.id)
return ".".join(reversed(parts))
if isinstance(node, ast.Subscript):
base = annotation_str(node.value)
idx = annotation_str(node.slice) if node.slice is not None else "?"
return f"{base}[{idx}]"
if isinstance(node, ast.Tuple):
parts = [annotation_str(e) for e in node.elts]
return "(" + ", ".join(parts) + ")"
if isinstance(node, ast.List):
parts = [annotation_str(e) for e in node.elts]
return "[" + ", ".join(parts) + "]"
if isinstance(node, (ast.Constant,)):
return repr(node.value)
if isinstance(node, ast.Starred):
return "*" + annotation_str(node.value)
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
return "|".join([annotation_str(node.left), annotation_str(node.right)])
return "Any"
def shorten(text, max_length):
text = (text or "").strip()
if len(text) <= max_length:
return text
if "\n" in text:
first, _, rest = text.partition("\n")
first = first.strip()
if len(first) <= max_length:
return first + "\n\n" + shorten(rest, max_length)
return text[:max_length - 1] + "…"
def collect(tree, out):
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
args = [a.arg for a in node.args.args if a.arg != "self"]
kwonly = [a.arg for a in node.args.kwonlyargs]
params = ", ".join(args + kwonly) or "(no params)"
ret = annotation_str(node.returns) if node.returns else "None"
doc = shorten(ast.get_docstring(node), 500)
out.append({"kind": "function", "name": node.name, "params": params, "return": ret, "doc": doc})
elif isinstance(node, ast.ClassDef):
doc = shorten(ast.get_docstring(node), 500)
methods = ", ".join(sorted(m.name for m in node.body if isinstance(m, (ast.FunctionDef, ast.AsyncFunctionDef))))
out.append({"kind": "class", "name": node.name, "methods": methods, "doc": doc})
elif isinstance(node, ast.Assign):
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
try:
doc = shorten(ast.get_docstring(node), 300)
except TypeError:
doc = ""
out.append({"kind": "variable", "name": node.targets[0].id, "value": annotation_str(node.value), "doc": doc})
def main():
files = load_py_files(sys.argv[1])
max_length = int(sys.argv[3]) if len(sys.argv) > 3 else 500
entries = []
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
except (SyntaxError, OSError):
continue
collect(tree, entries)
functions = [e for e in entries if e["kind"] == "function"]
classes = [e for e in entries if e["kind"] == "class"]
variables = [e for e in entries if e["kind"] == "variable"]
def section(title, items):
if not items:
return ""
md = ["## " + title, ""]
for it in items:
if it["kind"] == "class":
md.append(f"### `{it['name']}`")
md.append("")
md.append(it.get("doc", ""))
md.append("")
if it.get("methods"):
md.append("Methods: " + it["methods"])
else:
md.append(f"- **{it['name']}**({', '.join([p for p in [it.get('params')] if p])}) -> {it.get('return', 'None') or 'None'}")
md.append(it.get("doc", ""))
return "\n".join(md)
report = [
"# API Reference",
"",
f"Auto-generated from `{sys.argv[1]}`.",
"",
section("Classes", classes),
section("Functions", functions),
section("Module Variables", variables),
]
out_path = sys.argv[2].strip()
with open(out_path, "w", encoding="utf-8") as fh:
fh.write("\n".join(report))
json.dump({"outputPath": out_path, "entriesCount": len(functions) + len(classes) + len(variables), "bytesWritten": len("\n".join(report))}, sys.stdout, indent=2)
if __name__ == "__main__":
main()