"""StubGen 生成器模块 - 存根生成器主类和命令行入口""" from __future__ import annotations import sys import os import re import argparse import logging import traceback from pathlib import Path from typing import List from lib.core.VLogger import get_logger as _vlog from lib.core.stub_generator import CHeaderParser, PythonStubGenerator, CTypeMapper from lib.StubGen.Config import StubGenConfig from lib.StubGen.Converter import PythonToStubConverter class StubGen: """存根生成器主类""" def __init__(self, config: StubGenConfig) -> None: self.config: StubGenConfig = config self.logger: logging.Logger = self._SetupLogger() self.GeneratedFiles: List[str] = [] self.FailedFiles: List[str] = [] # 应用自定义类型映射 if config.TypeMappings: CTypeMapper.BASIC_TYPE_MAP.update(config.TypeMappings) def _SetupLogger(self) -> logging.Logger: """设置日志""" logger: logging.Logger = logging.getLogger('StubGen') logger.setLevel(logging.DEBUG if self.config.verbose else logging.INFO) if not logger.handlers: handler: logging.StreamHandler = logging.StreamHandler(sys.stdout) formatter: logging.Formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S' ) handler.setFormatter(formatter) logger.addHandler(handler) return logger def FindInputFiles(self) -> list[tuple[str, str]]: """查找输入文件,返回 (文件路径, 相对路径) 列表""" files: list[tuple[str, str]] = [] # 添加显式指定的文件 for FilePath in self.config.InputFiles: if os.path.exists(FilePath): RelPath: str = os.path.relpath(FilePath, self.config.InputDir or '.') files.append((FilePath, RelPath)) else: self.logger.warning(f"Input file not found: {FilePath}") # 从输入目录查找 if self.config.InputDir and os.path.exists(self.config.InputDir): InputPath: Path = Path(self.config.InputDir) for pattern in self.config.IncludePatterns: for FilePath in InputPath.rglob(pattern): # 检查是否在排除列表中 excluded: bool = False RelPath: str = os.path.relpath(str(FilePath), self.config.InputDir) for exclude_pattern in self.config.ExcludePatterns: if FilePath.match(exclude_pattern) or RelPath == exclude_pattern: excluded = True break if not excluded: files.append((str(FilePath), RelPath)) # 去重并保持顺序 seen: set[str] = set() UniqueFiles: list[tuple[str, str]] = [] for FilePath, RelPath in files: key: str = FilePath if key not in seen: seen.add(key) UniqueFiles.append((FilePath, RelPath)) return UniqueFiles def _GenerateGuardName(self, FilePath: str) -> str: """生成宏守卫名称""" # 获取文件名(不含扩展名) BaseName: str = os.path.splitext(os.path.basename(FilePath))[0] # 转换为大写,替换特殊字符为下划线 guard: str = re.sub(r'[^a-zA-Z0-9_]', '_', BaseName).upper() guard = re.sub(r'_+', '_', guard) # 合并多个下划线 guard = guard.strip('_') return f'{guard}_DEFINE_H' def _GetOutputPath(self, RelPath: str) -> str: """获取输出文件路径""" # 更改扩展名为 .pyi (Python stub file) BaseName: str = os.path.splitext(RelPath)[0] OutputRelPath: str = BaseName + '.pyi' # 构建完整输出路径 if self.config.OutputDir: OutputPath: str = os.path.join(self.config.OutputDir, OutputRelPath) else: OutputPath = OutputRelPath return OutputPath def GenerateStub(self, InputFile: str, RelPath: str) -> bool: """生成单个存根文件""" try: self.logger.info(f"Processing: {InputFile}") # 确定输出文件路径 OutputFile: str = self._GetOutputPath(RelPath) # 确保输出目录存在 OutputDir: str = os.path.dirname(OutputFile) if OutputDir and not os.path.exists(OutputDir): if not self.config.DryRun: os.makedirs(OutputDir, exist_ok=True) self.logger.debug(f"Created directory: {OutputDir}") if self.config.DryRun: self.logger.info(f"[DRY RUN] Would generate: {OutputFile}") return True # 根据文件类型选择处理方式 ext: str = os.path.splitext(InputFile)[1].lower() if ext in ['.h', '.c']: # C 文件:使用 CHeaderParser content: str = self._GenerateFromC(InputFile, RelPath) elif ext == '.py': # Python 文件:使用 PythonToStubConverter content = self._GenerateFromPy(InputFile, RelPath) else: self.logger.warning(f"Unsupported file type: {ext}") return False # 写入文件 with open(OutputFile, 'w', encoding='utf-8') as f: f.write(content) self.GeneratedFiles.append(OutputFile) self.logger.info(f"Generated: {OutputFile}") return True except Exception as e: self.logger.error(f"Failed to process {InputFile}: {e}") traceback.print_exc() self.FailedFiles.append(InputFile) return False def _GenerateFromC(self, InputFile: str, RelPath: str) -> str: """从 C 文件生成存根""" parser: CHeaderParser = CHeaderParser() parser.parse_file(InputFile) generator: PythonStubGenerator = PythonStubGenerator(parser) ModuleName: str = os.path.splitext(os.path.basename(InputFile))[0] content: str = generator.generate(ModuleName) # 添加宏守卫(如果需要) if self.config.GenerateGuards: guard_name: str = self._GenerateGuardName(InputFile) guard_comment: str = f"\n# Guard: {guard_name}\n" content = content + guard_comment return content def _GenerateFromPy(self, InputFile: str, RelPath: str) -> str: """从 Python 文件生成存根""" with open(InputFile, 'r', encoding='utf-8') as f: PyContent: str = f.read() ModuleName: str = os.path.splitext(os.path.basename(InputFile))[0] content: str = PythonToStubConverter.convert(PyContent, ModuleName) # 添加宏守卫(如果需要) if self.config.GenerateGuards: guard_name: str = self._GenerateGuardName(InputFile) guard_comment: str = f"\n# Guard: {guard_name}\n" content = content + guard_comment return content def run(self) -> bool: """运行生成器""" self.logger.info("=" * 60) self.logger.info("StubGen - C/H/Py to Python Stub Generator") self.logger.info("=" * 60) # 查找输入文件 InputFiles: list[tuple[str, str]] = self.FindInputFiles() if not InputFiles: self.logger.warning("No input files found!") return False self.logger.info(f"Found {len(InputFiles)} input file(s)") for FilePath, RelPath in InputFiles: self.logger.debug(f" - {RelPath}") # 处理每个文件 SuccessCount: int = 0 for FilePath, RelPath in InputFiles: if self.GenerateStub(FilePath, RelPath): SuccessCount += 1 # 输出统计信息 self.logger.info("=" * 60) self.logger.info(f"Summary: {SuccessCount}/{len(InputFiles)} files generated successfully") if self.FailedFiles: self.logger.warning(f"Failed files ({len(self.FailedFiles)}):") for f in self.FailedFiles: self.logger.warning(f" - {f}") return SuccessCount == len(InputFiles) def main() -> None: parser: argparse.ArgumentParser = argparse.ArgumentParser( description='StubGen - Generate Python stub files from C/H/Py sources', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples: # 使用配置文件 %(prog)s -c config.json # 处理单个文件 %(prog)s -i input.h -o output.py # 批量处理目录(保持目录结构) %(prog)s -d ./kernel -o ./kernel/include # 干运行(不实际生成文件) %(prog)s -d ./kernel --dry-run # 不生成宏守卫 %(prog)s -d ./kernel --no-guards ''' ) # 输入选项 InputGroup: argparse._MutuallyExclusiveGroup = parser.add_mutually_exclusive_group(required=True) InputGroup.add_argument('-c', '--config', help='Configuration file (JSON)') InputGroup.add_argument('-i', '--input', help='Input file') InputGroup.add_argument('-d', '--directory', help='Input directory') # 输出选项 parser.add_argument('-o', '--output', help='Output directory') # 其他选项 parser.add_argument('--include', action='append', help='Include patterns (default: *.h, *.c, *.py)') parser.add_argument('--exclude', action='append', help='Exclude patterns') parser.add_argument('--no-guards', action='store_true', help='Do not generate guard macros') parser.add_argument('--no-structure', action='store_true', help='Do not preserve directory structure') parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') parser.add_argument('--dry-run', action='store_true', help='Dry run (do not create files)') args: argparse.Namespace = parser.parse_args() # 加载配置 if args.config: if not os.path.exists(args.config): _vlog().error(f"配置文件未找到: {args.config}") sys.exit(1) config: StubGenConfig = StubGenConfig.from_file(args.config) else: config = StubGenConfig() config.IncludePatterns = args.include or ['*.h', '*.c', '*.py'] config.ExcludePatterns = args.exclude or [] config.GenerateGuards = not args.no_guards config.PreserveStructure = not args.no_structure config.verbose = args.verbose config.DryRun = args.dry_run if args.input: config.InputFiles = [args.input] if args.output: config.OutputDir = os.path.dirname(args.output) or '.' elif args.directory: config.InputDir = args.directory if args.output: config.OutputDir = args.output # 创建生成器并运行 generator: StubGen = StubGen(config) # 如果指定了单个输出文件,直接处理 if args.input and args.output and not os.path.isdir(args.output): RelPath: str = os.path.relpath(args.input, config.InputDir or '.') success: bool = generator.GenerateStub(args.input, RelPath) else: success = generator.run() sys.exit(0 if success else 1) if __name__ == '__main__': main()