#!/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""" McRogueFace API Reference

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

""" # Generate function documentation for func_name in sorted(functions.keys()): func_info = functions[func_name] parsed = func_info["parsed"] html_content += f"""

{func_name}{parsed['signature'] if parsed['signature'] else '(...)'}

{html.escape(parsed['description'])}

""" if parsed['args']: html_content += "

Arguments:

\n \n" if parsed['returns']: html_content += f"

Returns: {html.escape(parsed['returns'])}

\n" if parsed['example']: html_content += f"

Example:

\n
{html.escape(parsed['example'])}
\n" html_content += "
\n" # Generate class documentation html_content += "\n

Classes

\n" for class_name in sorted(classes.keys()): class_info = classes[class_name] html_content += f"""

{class_name}

""" if class_info['bases']: html_content += f"

Inherits from: {', '.join(class_info['bases'])}

\n" if class_info['doc']: html_content += f"

{html.escape(class_info['doc'])}

\n" # Properties if class_info['properties']: html_content += "

Properties:

\n \n" # Methods if class_info['methods']: html_content += "

Methods:

\n" for method_name, method_info in sorted(class_info['methods'].items()): if method_name == '__init__': continue parsed = method_info['parsed'] html_content += f"""
{method_name}{parsed['signature'] if parsed['signature'] else '(...)'}
""" if parsed['description']: html_content += f"

{html.escape(parsed['description'])}

\n" if parsed['args']: html_content += "
\n" for arg in parsed['args']: html_content += f"
{arg['name']}: {html.escape(arg['description'])}
\n" html_content += "
\n" if parsed['returns']: html_content += f"

Returns: {html.escape(parsed['returns'])}

\n" html_content += "
\n" html_content += "
\n" # Constants html_content += "\n

Constants

\n \n" html_content += """
""" # 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!")