McRogueFace/tools/generate_dynamic_docs.py

510 lines
18 KiB
Python

#!/usr/bin/env python3
"""
Dynamic documentation generator for McRogueFace.
Extracts all documentation directly from the compiled module using introspection.
"""
import os
import sys
import inspect
import datetime
import html
import re
from pathlib import Path
# Must be run with McRogueFace as interpreter
try:
import mcrfpy
except ImportError:
print("Error: This script must be run with McRogueFace as the interpreter")
print("Usage: ./build/mcrogueface --exec generate_dynamic_docs.py")
sys.exit(1)
def parse_docstring(docstring):
"""Parse a docstring to extract signature, description, args, and returns."""
if not docstring:
return {"signature": "", "description": "", "args": [], "returns": "", "example": ""}
lines = docstring.strip().split('\n')
result = {
"signature": "",
"description": "",
"args": [],
"returns": "",
"example": ""
}
# First line often contains the signature
if lines and '(' in lines[0] and ')' in lines[0]:
result["signature"] = lines[0].strip()
lines = lines[1:] if len(lines) > 1 else []
# Parse the rest
current_section = "description"
description_lines = []
example_lines = []
in_example = False
for line in lines:
line_lower = line.strip().lower()
if line_lower.startswith("args:") or line_lower.startswith("arguments:"):
current_section = "args"
continue
elif line_lower.startswith("returns:") or line_lower.startswith("return:"):
current_section = "returns"
result["returns"] = line[line.find(':')+1:].strip()
continue
elif line_lower.startswith("example:") or line_lower.startswith("examples:"):
in_example = True
continue
elif line_lower.startswith("note:"):
if description_lines:
description_lines.append("")
description_lines.append(line)
continue
if in_example:
example_lines.append(line)
elif current_section == "description" and not line.startswith(" "):
description_lines.append(line)
elif current_section == "args" and line.strip():
# Parse argument lines like " x: X coordinate"
match = re.match(r'\s+(\w+):\s*(.+)', line)
if match:
result["args"].append({
"name": match.group(1),
"description": match.group(2).strip()
})
elif current_section == "returns" and line.strip() and line.startswith(" "):
result["returns"] += " " + line.strip()
result["description"] = '\n'.join(description_lines).strip()
result["example"] = '\n'.join(example_lines).strip()
return result
def get_all_functions():
"""Get all module-level functions."""
functions = {}
for name in dir(mcrfpy):
if name.startswith('_'):
continue
obj = getattr(mcrfpy, name)
if inspect.isbuiltin(obj) or inspect.isfunction(obj):
doc_info = parse_docstring(obj.__doc__)
functions[name] = {
"name": name,
"doc": obj.__doc__ or "",
"parsed": doc_info
}
return functions
def get_all_classes():
"""Get all classes and their methods/properties."""
classes = {}
for name in dir(mcrfpy):
if name.startswith('_'):
continue
obj = getattr(mcrfpy, name)
if inspect.isclass(obj):
class_info = {
"name": name,
"doc": obj.__doc__ or "",
"methods": {},
"properties": {},
"bases": [base.__name__ for base in obj.__bases__ if base.__name__ != 'object']
}
# Get methods and properties
for attr_name in dir(obj):
if attr_name.startswith('__') and attr_name != '__init__':
continue
try:
attr = getattr(obj, attr_name)
if callable(attr):
method_doc = attr.__doc__ or ""
class_info["methods"][attr_name] = {
"doc": method_doc,
"parsed": parse_docstring(method_doc)
}
elif isinstance(attr, property):
prop_doc = (attr.fget.__doc__ if attr.fget else "") or ""
class_info["properties"][attr_name] = {
"doc": prop_doc,
"readonly": attr.fset is None
}
except:
pass
classes[name] = class_info
return classes
def get_constants():
"""Get module constants."""
constants = {}
for name in dir(mcrfpy):
if name.startswith('_') or name[0].islower():
continue
obj = getattr(mcrfpy, name)
if not (inspect.isclass(obj) or callable(obj)):
constants[name] = {
"name": name,
"value": repr(obj) if not name.startswith('default_') else f"<{name}>",
"type": type(obj).__name__
}
return constants
def generate_html_docs():
"""Generate HTML documentation."""
functions = get_all_functions()
classes = get_all_classes()
constants = get_constants()
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>McRogueFace API Reference</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}}
.container {{
background-color: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
h1, h2, h3, h4, h5 {{
color: #2c3e50;
}}
.toc {{
background-color: #f8f9fa;
padding: 20px;
border-radius: 4px;
margin-bottom: 30px;
}}
.toc ul {{
list-style-type: none;
padding-left: 20px;
}}
.toc > ul {{
padding-left: 0;
}}
.toc a {{
text-decoration: none;
color: #3498db;
}}
.toc a:hover {{
text-decoration: underline;
}}
.method-section {{
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 4px;
border-left: 4px solid #3498db;
}}
.function-signature {{
font-family: 'Consolas', 'Monaco', monospace;
background-color: #e9ecef;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}}
.class-name {{
color: #e74c3c;
font-weight: bold;
}}
.method-name {{
color: #3498db;
font-family: 'Consolas', 'Monaco', monospace;
}}
.property-name {{
color: #27ae60;
font-family: 'Consolas', 'Monaco', monospace;
}}
.arg-name {{
color: #8b4513;
font-weight: bold;
}}
.arg-type {{
color: #666;
font-style: italic;
}}
code {{
background-color: #f4f4f4;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
}}
pre {{
background-color: #f4f4f4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
}}
.deprecated {{
text-decoration: line-through;
opacity: 0.6;
}}
.note {{
background-color: #fff3cd;
border-left: 4px solid #ffc107;
padding: 10px;
margin: 10px 0;
}}
.returns {{
color: #28a745;
font-weight: bold;
}}
</style>
</head>
<body>
<div class="container">
<h1>McRogueFace API Reference</h1>
<p><em>Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</em></p>
<p><em>This documentation was dynamically generated from the compiled module.</em></p>
<div class="toc">
<h2>Table of Contents</h2>
<ul>
<li><a href="#functions">Functions</a></li>
<li><a href="#classes">Classes</a>
<ul>
"""
# Add classes to TOC
for class_name in sorted(classes.keys()):
html_content += f' <li><a href="#{class_name}">{class_name}</a></li>\n'
html_content += """ </ul>
</li>
<li><a href="#constants">Constants</a></li>
</ul>
</div>
<h2 id="functions">Functions</h2>
"""
# Generate function documentation
for func_name in sorted(functions.keys()):
func_info = functions[func_name]
parsed = func_info["parsed"]
html_content += f"""
<div class="method-section">
<h3><code class="function-signature">{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h3>
<p>{html.escape(parsed['description'])}</p>
"""
if parsed['args']:
html_content += " <h4>Arguments:</h4>\n <ul>\n"
for arg in parsed['args']:
html_content += f" <li><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</li>\n"
html_content += " </ul>\n"
if parsed['returns']:
html_content += f" <p><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
if parsed['example']:
html_content += f" <h4>Example:</h4>\n <pre><code>{html.escape(parsed['example'])}</code></pre>\n"
html_content += " </div>\n"
# Generate class documentation
html_content += "\n <h2 id='classes'>Classes</h2>\n"
for class_name in sorted(classes.keys()):
class_info = classes[class_name]
html_content += f"""
<div class="method-section">
<h3 id="{class_name}"><span class="class-name">{class_name}</span></h3>
"""
if class_info['bases']:
html_content += f" <p><em>Inherits from: {', '.join(class_info['bases'])}</em></p>\n"
if class_info['doc']:
html_content += f" <p>{html.escape(class_info['doc'])}</p>\n"
# Properties
if class_info['properties']:
html_content += " <h4>Properties:</h4>\n <ul>\n"
for prop_name, prop_info in sorted(class_info['properties'].items()):
readonly = " (read-only)" if prop_info['readonly'] else ""
html_content += f" <li><span class='property-name'>{prop_name}</span>{readonly}"
if prop_info['doc']:
html_content += f": {html.escape(prop_info['doc'])}"
html_content += "</li>\n"
html_content += " </ul>\n"
# Methods
if class_info['methods']:
html_content += " <h4>Methods:</h4>\n"
for method_name, method_info in sorted(class_info['methods'].items()):
if method_name == '__init__':
continue
parsed = method_info['parsed']
html_content += f"""
<div style="margin-left: 20px; margin-bottom: 15px;">
<h5><code class="method-name">{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}</code></h5>
"""
if parsed['description']:
html_content += f" <p>{html.escape(parsed['description'])}</p>\n"
if parsed['args']:
html_content += " <div style='margin-left: 20px;'>\n"
for arg in parsed['args']:
html_content += f" <div><span class='arg-name'>{arg['name']}</span>: {html.escape(arg['description'])}</div>\n"
html_content += " </div>\n"
if parsed['returns']:
html_content += f" <p style='margin-left: 20px;'><span class='returns'>Returns:</span> {html.escape(parsed['returns'])}</p>\n"
html_content += " </div>\n"
html_content += " </div>\n"
# Constants
html_content += "\n <h2 id='constants'>Constants</h2>\n <ul>\n"
for const_name, const_info in sorted(constants.items()):
html_content += f" <li><code>{const_name}</code> ({const_info['type']}): {const_info['value']}</li>\n"
html_content += " </ul>\n"
html_content += """
</div>
</body>
</html>
"""
# Write the file
output_path = Path("docs/api_reference_dynamic.html")
output_path.parent.mkdir(exist_ok=True)
output_path.write_text(html_content)
print(f"Generated {output_path}")
print(f"Found {len(functions)} functions, {len(classes)} classes, {len(constants)} constants")
def generate_markdown_docs():
"""Generate Markdown documentation."""
functions = get_all_functions()
classes = get_all_classes()
constants = get_constants()
md_content = f"""# McRogueFace API Reference
*Generated on {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
*This documentation was dynamically generated from the compiled module.*
## Table of Contents
- [Functions](#functions)
- [Classes](#classes)
"""
# Add classes to TOC
for class_name in sorted(classes.keys()):
md_content += f" - [{class_name}](#{class_name.lower()})\n"
md_content += "- [Constants](#constants)\n\n"
# Functions
md_content += "## Functions\n\n"
for func_name in sorted(functions.keys()):
func_info = functions[func_name]
parsed = func_info["parsed"]
md_content += f"### `{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
if parsed['description']:
md_content += f"{parsed['description']}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"
for arg in parsed['args']:
md_content += f"- `{arg['name']}`: {arg['description']}\n"
md_content += "\n"
if parsed['returns']:
md_content += f"**Returns:** {parsed['returns']}\n\n"
if parsed['example']:
md_content += f"**Example:**\n```python\n{parsed['example']}\n```\n\n"
# Classes
md_content += "## Classes\n\n"
for class_name in sorted(classes.keys()):
class_info = classes[class_name]
md_content += f"### {class_name}\n\n"
if class_info['bases']:
md_content += f"*Inherits from: {', '.join(class_info['bases'])}*\n\n"
if class_info['doc']:
md_content += f"{class_info['doc']}\n\n"
# Properties
if class_info['properties']:
md_content += "**Properties:**\n"
for prop_name, prop_info in sorted(class_info['properties'].items()):
readonly = " *(read-only)*" if prop_info['readonly'] else ""
md_content += f"- `{prop_name}`{readonly}"
if prop_info['doc']:
md_content += f": {prop_info['doc']}"
md_content += "\n"
md_content += "\n"
# Methods
if class_info['methods']:
md_content += "**Methods:**\n\n"
for method_name, method_info in sorted(class_info['methods'].items()):
if method_name == '__init__':
continue
parsed = method_info['parsed']
md_content += f"#### `{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}`\n\n"
if parsed['description']:
md_content += f"{parsed['description']}\n\n"
if parsed['args']:
md_content += "**Arguments:**\n"
for arg in parsed['args']:
md_content += f"- `{arg['name']}`: {arg['description']}\n"
md_content += "\n"
if parsed['returns']:
md_content += f"**Returns:** {parsed['returns']}\n\n"
# Constants
md_content += "## Constants\n\n"
for const_name, const_info in sorted(constants.items()):
md_content += f"- `{const_name}` ({const_info['type']}): {const_info['value']}\n"
# Write the file
output_path = Path("docs/API_REFERENCE_DYNAMIC.md")
output_path.parent.mkdir(exist_ok=True)
output_path.write_text(md_content)
print(f"Generated {output_path}")
if __name__ == "__main__":
print("Generating dynamic documentation from mcrfpy module...")
generate_html_docs()
generate_markdown_docs()
print("Documentation generation complete!")