McRogueFace/generate_stubs.py

268 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Generate .pyi type stub files for McRogueFace Python API.
This script introspects the mcrfpy module and generates type stubs
for better IDE support and type checking.
"""
import os
import sys
import inspect
import types
from typing import Dict, List, Set, Any
# Add the build directory to path to import mcrfpy
sys.path.insert(0, './build')
try:
import mcrfpy
except ImportError:
print("Error: Could not import mcrfpy. Make sure to run this from the project root after building.")
sys.exit(1)
def parse_docstring_signature(doc: str) -> tuple[str, str]:
"""Extract signature and description from docstring."""
if not doc:
return "", ""
lines = doc.strip().split('\n')
if lines:
# First line often contains the signature
first_line = lines[0]
if '(' in first_line and ')' in first_line:
# Extract just the part after the function name
start = first_line.find('(')
end = first_line.rfind(')') + 1
if start != -1 and end != 0:
sig = first_line[start:end]
# Get return type if present
if '->' in first_line:
ret_start = first_line.find('->')
ret_type = first_line[ret_start:].strip()
return sig, ret_type
return sig, ""
return "", ""
def get_type_hint(obj_type: type) -> str:
"""Convert Python type to type hint string."""
if obj_type == int:
return "int"
elif obj_type == float:
return "float"
elif obj_type == str:
return "str"
elif obj_type == bool:
return "bool"
elif obj_type == list:
return "List[Any]"
elif obj_type == dict:
return "Dict[Any, Any]"
elif obj_type == tuple:
return "Tuple[Any, ...]"
elif obj_type == type(None):
return "None"
else:
return "Any"
def generate_class_stub(class_name: str, cls: type) -> List[str]:
"""Generate stub for a class."""
lines = []
# Get class docstring
if cls.__doc__:
doc_lines = cls.__doc__.strip().split('\n')
# Use only the first paragraph for the stub
lines.append(f'class {class_name}:')
lines.append(f' """{doc_lines[0]}"""')
else:
lines.append(f'class {class_name}:')
# Check for __init__ method
if hasattr(cls, '__init__'):
init_doc = cls.__init__.__doc__ or cls.__doc__
if init_doc:
sig, ret = parse_docstring_signature(init_doc)
if sig:
lines.append(f' def __init__(self{sig[1:-1]}) -> None: ...')
else:
lines.append(f' def __init__(self, *args, **kwargs) -> None: ...')
else:
lines.append(f' def __init__(self, *args, **kwargs) -> None: ...')
# Get properties and methods
properties = []
methods = []
for attr_name in dir(cls):
if attr_name.startswith('_') and not attr_name.startswith('__'):
continue
try:
attr = getattr(cls, attr_name)
if isinstance(attr, property):
properties.append((attr_name, attr))
elif callable(attr) and not attr_name.startswith('__'):
methods.append((attr_name, attr))
except:
pass
# Add properties
if properties:
lines.append('')
for prop_name, prop in properties:
# Try to determine property type from docstring
if prop.fget and prop.fget.__doc__:
lines.append(f' @property')
lines.append(f' def {prop_name}(self) -> Any: ...')
if prop.fset:
lines.append(f' @{prop_name}.setter')
lines.append(f' def {prop_name}(self, value: Any) -> None: ...')
else:
lines.append(f' {prop_name}: Any')
# Add methods
if methods:
lines.append('')
for method_name, method in methods:
if method.__doc__:
sig, ret = parse_docstring_signature(method.__doc__)
if sig and ret:
lines.append(f' def {method_name}(self{sig[1:-1]}) {ret}: ...')
elif sig:
lines.append(f' def {method_name}(self{sig[1:-1]}) -> Any: ...')
else:
lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...')
else:
lines.append(f' def {method_name}(self, *args, **kwargs) -> Any: ...')
lines.append('')
return lines
def generate_function_stub(func_name: str, func: Any) -> str:
"""Generate stub for a function."""
if func.__doc__:
sig, ret = parse_docstring_signature(func.__doc__)
if sig and ret:
return f'def {func_name}{sig} {ret}: ...'
elif sig:
return f'def {func_name}{sig} -> Any: ...'
return f'def {func_name}(*args, **kwargs) -> Any: ...'
def generate_stubs():
"""Generate the main mcrfpy.pyi file."""
lines = [
'"""Type stubs for McRogueFace Python API.',
'',
'Auto-generated - do not edit directly.',
'"""',
'',
'from typing import Any, List, Dict, Tuple, Optional, Callable, Union',
'',
'# Module documentation',
]
# Add module docstring as comment
if mcrfpy.__doc__:
for line in mcrfpy.__doc__.strip().split('\n')[:3]:
lines.append(f'# {line}')
lines.extend(['', '# Classes', ''])
# Collect all classes
classes = []
functions = []
constants = []
for name in sorted(dir(mcrfpy)):
if name.startswith('_'):
continue
obj = getattr(mcrfpy, name)
if isinstance(obj, type):
classes.append((name, obj))
elif callable(obj):
functions.append((name, obj))
elif not inspect.ismodule(obj):
constants.append((name, obj))
# Generate class stubs
for class_name, cls in classes:
lines.extend(generate_class_stub(class_name, cls))
# Generate function stubs
if functions:
lines.extend(['# Functions', ''])
for func_name, func in functions:
lines.append(generate_function_stub(func_name, func))
lines.append('')
# Generate constants
if constants:
lines.extend(['# Constants', ''])
for const_name, const in constants:
const_type = get_type_hint(type(const))
lines.append(f'{const_name}: {const_type}')
return '\n'.join(lines)
def generate_automation_stubs():
"""Generate stubs for the automation submodule."""
if not hasattr(mcrfpy, 'automation'):
return None
automation = mcrfpy.automation
lines = [
'"""Type stubs for McRogueFace automation API."""',
'',
'from typing import Optional, Tuple',
'',
]
# Get all automation functions
for name in sorted(dir(automation)):
if name.startswith('_'):
continue
obj = getattr(automation, name)
if callable(obj):
lines.append(generate_function_stub(name, obj))
return '\n'.join(lines)
def main():
"""Main entry point."""
print("Generating type stubs for McRogueFace...")
# Generate main module stubs
stubs = generate_stubs()
# Create stubs directory
os.makedirs('stubs', exist_ok=True)
# Write main module stubs
with open('stubs/mcrfpy.pyi', 'w') as f:
f.write(stubs)
print("Generated stubs/mcrfpy.pyi")
# Generate automation module stubs if available
automation_stubs = generate_automation_stubs()
if automation_stubs:
os.makedirs('stubs/mcrfpy', exist_ok=True)
with open('stubs/mcrfpy/__init__.pyi', 'w') as f:
f.write(stubs)
with open('stubs/mcrfpy/automation.pyi', 'w') as f:
f.write(automation_stubs)
print("Generated stubs/mcrfpy/automation.pyi")
print("\nType stubs generated successfully!")
print("\nTo use in your IDE:")
print("1. Add the 'stubs' directory to your PYTHONPATH")
print("2. Or configure your IDE to look for stubs in the 'stubs' directory")
print("3. Most IDEs will automatically detect .pyi files")
if __name__ == '__main__':
main()