268 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			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() |