#!/usr/bin/env python3 """Update `simplecadapi/__init__.py` exports from source modules. This tool supports two common workflows: - `--show-api-only`: inspect the public API grouped by module and category. - default mode: regenerate `__init__.py` from the current module set. It reads module files directly from the installed package location, so it works both from a source checkout and from an installed virtual environment. """ from __future__ import annotations import argparse import ast from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Mapping, Sequence, Tuple PACKAGE_NAME = "simplecadapi" PACKAGE_ROOT = Path(__file__).resolve().parents[1] INIT_FILE = PACKAGE_ROOT / "__init__.py" CORE_EXPORTS = [ "CoordinateSystem", "SimpleWorkplane", "Vertex", "Edge", "Wire", "Face", "Solid", "AnyShape", "TaggedMixin", "get_current_cs", "WORLD_CS", ] MODULE_ORDER = ("operations", "evolve", "ql") MODULE_LABELS = { "operations": "Operations", "evolve": "Evolve", "ql": "QL", } FUNCTION_CATEGORIES = { "基础几何创建": [ "make_point_", "make_line_", "make_segment_", "make_circle_", "make_rectangle_", "make_box_", "make_cylinder_", "make_sphere_", "make_angle_arc_", "make_three_point_arc_", "make_spline_", "make_polyline_", "make_helix_", "make_face_from_wire_", "make_wire_from_edges_", "make_cone_", ], "变换操作": ["translate_", "rotate_", "scale_", "mirror_"], "3D操作": ["extrude_", "revolve_", "loft_", "sweep_", "helical_sweep_"], "标签和选择": ["apply_tag", "list_tags", "select_faces_", "select_edges_"], "布尔运算": ["union_", "cut_", "intersect_", "difference_"], "导出": ["export_", "render_"], "高级特征操作": ["fillet_", "chamfer_", "shell_", "pattern_", "array_"], } QL_CATEGORIES = { "标签与元数据谓词": ["tag", "meta", "geo"], "逻辑组合": ["and_", "or_", "not_"], "查询与取值": ["select", "value"], } ALIAS_RULES = { "make_point_rvertex": "create_point", "make_line_redge": "create_line", "make_segment_redge": "create_segment", "make_segment_rwire": "create_segment_wire", "make_circle_redge": "create_circle_edge", "make_circle_rwire": "create_circle_wire", "make_circle_rface": "create_circle_face", "make_rectangle_rwire": "create_rectangle_wire", "make_rectangle_rface": "create_rectangle_face", "make_box_rsolid": "create_box", "make_cylinder_rsolid": "create_cylinder", "make_sphere_rsolid": "create_sphere", "make_angle_arc_redge": "create_angle_arc", "make_angle_arc_rwire": "create_angle_arc_wire", "make_three_point_arc_redge": "create_arc", "make_three_point_arc_rwire": "create_arc_wire", "make_spline_redge": "create_spline", "make_spline_rwire": "create_spline_wire", "make_polyline_rwire": "create_polyline_wire", "make_helix_redge": "create_helix", "make_helix_rwire": "create_helix_wire", "make_face_from_wire_rface": "create_face_from_wire", "make_wire_from_edges_rwire": "create_wire_from_edges", "translate_shape": "translate", "rotate_shape": "rotate", "extrude_rsolid": "extrude", "revolve_rsolid": "revolve", "union_rsolid": "union", "cut_rsolid": "cut", "intersect_rsolid": "intersect", "export_step": "to_step", "export_stl": "to_stl", } @dataclass(frozen=True) class ModuleInventory: name: str display_name: str functions: List[str] = field(default_factory=list) classes: List[str] = field(default_factory=list) def _module_file(module_name: str, package_root: Path | None = None) -> Path: root = package_root.resolve() if package_root is not None else PACKAGE_ROOT return root / f"{module_name}.py" def _parse_module(file_path: Path) -> ast.Module | None: if not file_path.exists(): print(f"警告: {file_path} 文件不存在") return None try: source = file_path.read_text(encoding="utf-8") return ast.parse(source, filename=str(file_path)) except Exception as exc: print(f"警告: 解析 {file_path} 失败: {exc}") return None def extract_public_functions(file_path: Path) -> List[str]: tree = _parse_module(file_path) if tree is None: return [] functions: List[str] = [] for node in tree.body: if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"): functions.append(node.name) return functions def extract_public_classes(file_path: Path) -> List[str]: tree = _parse_module(file_path) if tree is None: return [] classes: List[str] = [] for node in tree.body: if isinstance(node, ast.ClassDef) and not node.name.startswith("_"): classes.append(node.name) return classes def collect_api_inventory( package_root: Path | None = None, ) -> Dict[str, ModuleInventory]: root = package_root.resolve() if package_root is not None else PACKAGE_ROOT inventory: Dict[str, ModuleInventory] = {} for module_name in MODULE_ORDER: file_path = _module_file(module_name, root) inventory[module_name] = ModuleInventory( name=module_name, display_name=MODULE_LABELS[module_name], functions=extract_public_functions(file_path), classes=[], ) return inventory def categorize_functions( functions: Sequence[str], category_rules: Mapping[str, Sequence[str]], fallback_label: str = "其他", ) -> Dict[str, List[str]]: categorized: Dict[str, List[str]] = {name: [] for name in category_rules} for func in functions: matched = False for category, prefixes in category_rules.items(): if any(func.startswith(prefix) for prefix in prefixes): categorized[category].append(func) matched = True break if not matched: categorized.setdefault(fallback_label, []).append(func) return {name: sorted(funcs) for name, funcs in categorized.items() if funcs} def categorize_module( module_name: str, functions: Sequence[str] ) -> Dict[str, List[str]]: if module_name in {"operations", "evolve"}: return categorize_functions(functions, FUNCTION_CATEGORIES) if module_name == "ql": return categorize_functions(functions, QL_CATEGORIES) return {"其他": list(functions)} if functions else {} def generate_core_imports() -> str: lines = ["from .core import (", " # 核心类"] for name in CORE_EXPORTS[:-2]: lines.append(f" {name},") lines.append(" # 坐标系函数") for name in CORE_EXPORTS[-2:]: lines.append(f" {name},") lines.append(")") return "\n".join(lines) def _render_import_block(module_name: str, categorized: Dict[str, List[str]]) -> str: lines = [f"from .{module_name} import ("] for category, functions in categorized.items(): lines.append(f" # {category}") for func in functions: lines.append(f" {func},") lines.append("") if lines[-1] == "": lines.pop() lines.append(")") return "\n".join(lines) def generate_aliases(functions: Sequence[str]) -> str: alias_lines = ["# 便于使用的别名", "Workplane = SimpleWorkplane", ""] alias_categories: Dict[str, List[Tuple[str, str]]] = {} for func in functions: alias = ALIAS_RULES.get(func) if alias is None: continue if func.startswith("make_"): category = "创建函数别名" elif func.startswith(("translate_", "rotate_")): category = "变换操作别名" elif func.startswith(("extrude_", "revolve_")): category = "3D操作别名" elif func.startswith(("union_", "cut_", "intersect_")): category = "布尔运算别名" elif func.startswith("export_"): category = "导出别名" else: category = "其他别名" alias_categories.setdefault(category, []).append((func, alias)) for category, aliases in alias_categories.items(): alias_lines.append(f"# {category}") for func, alias in sorted(aliases, key=lambda item: item[1]): alias_lines.append(f"{alias} = {func}") alias_lines.append("") if alias_lines[-1] == "": alias_lines.pop() return "\n".join(alias_lines) def generate_all_list( operations_functions: Sequence[str], evolve_functions: Sequence[str], ) -> str: all_lines = ["__all__ = [", " # 核心类"] core_public = [ "CoordinateSystem", "SimpleWorkplane", "Workplane", "Vertex", "Edge", "Wire", "Face", "Solid", "AnyShape", "TaggedMixin", ] for name in core_public: all_lines.append(f' "{name}",') all_lines.extend( ["", " # 坐标系", ' "get_current_cs",', ' "WORLD_CS",', ""] ) public_functions = list(operations_functions) + list(evolve_functions) categorized = categorize_functions(public_functions, FUNCTION_CATEGORIES) for category, funcs in categorized.items(): all_lines.append(f" # {category}") for func in funcs: all_lines.append(f' "{func}",') all_lines.append("") all_lines.extend([' "ql",', ' "translator",', "", " # 别名"]) aliases = sorted( ALIAS_RULES[func] for func in public_functions if func in ALIAS_RULES ) for alias in aliases: all_lines.append(f' "{alias}",') all_lines.append("]") return "\n".join(all_lines) def generate_init_file(inventory: Dict[str, ModuleInventory]) -> str: operations = inventory["operations"].functions evolve = inventory["evolve"].functions lines = [ '"""', "SimpleCAD API - 简化的CAD建模Python API", "基于 OCP 实现,提供直观的几何建模接口", '"""', "", generate_core_imports(), "", ] if operations: lines.append( _render_import_block( "operations", categorize_module("operations", operations) ) ) lines.append("") if evolve: lines.append( _render_import_block("evolve", categorize_module("evolve", evolve)) ) lines.append("") lines.extend( [ "from . import ql", "from . import translator", "", '__author__ = "SimpleCAD API Team"', '__description__ = "Simplified OCP-native CAD modeling Python API"', "", generate_aliases(list(operations) + list(evolve)), "", generate_all_list(operations, evolve), "", ] ) return "\n".join(lines) def backup_init_file(file_path: Path = INIT_FILE) -> None: if not file_path.exists(): return backup_file = file_path.with_suffix(".py.bak") backup_file.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8") print(f"已备份原文件到: {backup_file}") def check_syntax(file_path: Path) -> bool: try: ast.parse(file_path.read_text(encoding="utf-8"), filename=str(file_path)) return True except SyntaxError as exc: print(f"语法错误: {exc}") return False except Exception as exc: print(f"文件检查失败: {exc}") return False def extract_existing_auto_exports(file_path: Path = INIT_FILE) -> List[str]: if not file_path.exists(): return [] try: tree = ast.parse(file_path.read_text(encoding="utf-8"), filename=str(file_path)) except Exception as exc: print(f"比较文件时出错: {exc}") return [] symbols: List[str] = [] managed_modules = {"operations", "evolve"} for node in tree.body: if not isinstance(node, ast.ImportFrom) or node.level != 1: continue if node.module in managed_modules: for alias in node.names: if alias.name != "*": symbols.append(alias.name) continue if node.module is None: for alias in node.names: if alias.name in {"field", "ql", "translator"}: symbols.append(alias.name) return sorted(dict.fromkeys(symbols)) def compare_with_existing(target_symbols: Sequence[str]) -> Tuple[List[str], List[str]]: existing = extract_existing_auto_exports() target = sorted(dict.fromkeys(target_symbols)) new_additions = [name for name in target if name not in existing] removed = [name for name in existing if name not in target] return new_additions, removed def parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser( description="自动更新 SimpleCAD API 的 __init__.py 文件", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 使用示例: python make_export.py # 标准模式 python make_export.py --dry-run # 预览模式,不实际修改文件 python make_export.py --show-api-only # 只显示 API 列表 python make_export.py --force # 强制模式,跳过确认 python make_export.py --verbose # 详细输出模式 """, ) parser.add_argument( "--dry-run", action="store_true", help="预览模式,显示将要进行的更改但不实际修改文件", ) parser.add_argument( "--show-api-only", action="store_true", help="仅显示 API 函数,不生成 __init__.py 文件", ) parser.add_argument( "--force", action="store_true", help="强制模式,跳过所有确认提示", ) parser.add_argument( "--verbose", "-v", action="store_true", help="详细输出模式", ) parser.add_argument( "--backup", dest="backup", action="store_true", default=True, help="创建备份文件 (默认: 创建备份)", ) parser.add_argument( "--no-backup", dest="backup", action="store_false", help="不创建备份文件", ) return parser.parse_args() def _print_module_summary(module_name: str, inventory: ModuleInventory) -> int: categorized = categorize_module(module_name, inventory.functions) total = 0 print(f"\n🔹 {inventory.display_name} 模块:") for category, funcs in categorized.items(): print(f"\n {category} ({len(funcs)} 个函数):") for func in sorted(funcs): print(f" - {func}") total += len(funcs) if inventory.classes: print(f"\n 导出类 ({len(inventory.classes)} 个类):") for cls in inventory.classes: print(f" - {cls}") return total def _verbose_module_summary(module_name: str, inventory: ModuleInventory) -> None: categorized = categorize_module(module_name, inventory.functions) print(f" {inventory.display_name} 模块:") for category, funcs in categorized.items(): print(f" {category}: {len(funcs)} 个函数") for func in funcs[:3]: print(f" - {func}") if len(funcs) > 3: print(f" ... 和其他 {len(funcs) - 3} 个函数") if inventory.classes: print(f" 导出类: {len(inventory.classes)} 个") def _target_symbols(inventory: Dict[str, ModuleInventory]) -> List[str]: symbols: List[str] = [] for module_name in ("operations", "evolve"): symbols.extend(inventory[module_name].functions) symbols.extend(inventory[module_name].classes) symbols.append("ql") symbols.append("translator") return symbols def main() -> None: args = parse_arguments() if args.verbose: print("🚀 开始更新 __init__.py 文件...") print(f"📦 包目录: {PACKAGE_ROOT}") inventory = collect_api_inventory() operations_functions = inventory["operations"].functions evolve_functions = inventory["evolve"].functions if not any(item.functions or item.classes for item in inventory.values()): print("❌ 未找到任何公共 API,退出") return target_symbols = _target_symbols(inventory) new_additions, removed_functions = compare_with_existing(target_symbols) if args.verbose: print("\n📊 函数分类统计:") for module_name in MODULE_ORDER: _verbose_module_summary(module_name, inventory[module_name]) if new_additions and args.verbose: print(f"\n🆕 新增导出 ({len(new_additions)} 个):") for name in new_additions: print(f" + {name}") if removed_functions and args.verbose: print(f"\n🗑️ 删除导出 ({len(removed_functions)} 个):") for name in removed_functions: print(f" - {name}") if args.show_api_only: print("\n📜 API 函数列表 (按模块和类别分组):") totals: Dict[str, int] = {} for module_name in MODULE_ORDER: totals[module_name] = _print_module_summary( module_name, inventory[module_name] ) print( "\n📊 总计: " f"Operations {totals['operations']} 个函数, " f"Evolve {totals['evolve']} 个函数, " f"QL {totals['ql']} 个函数, " f"总计 {sum(totals.values())} 个函数" ) return new_content = generate_init_file(inventory) if args.dry_run: print("\n👁️ 预览模式 - 将要进行的更改:") print(f" 生成的文件大小: {len(new_content)} 字符") print(f" Operations 函数数: {len(operations_functions)}") print(f" Evolve 函数数: {len(evolve_functions)}") print(f" QL 函数数: {len(inventory['ql'].functions)}") print(f" 总导出符号数: {len(target_symbols)}") print( f" 别名数: {len([name for name in operations_functions + evolve_functions if name in ALIAS_RULES])}" ) print(" (使用 --verbose 查看详细信息)") print("\n💡 要实际执行更改,请移除 --dry-run 参数") return if args.backup: backup_init_file() print("\n🔄 生成新的 __init__.py 文件...") INIT_FILE.write_text(new_content, encoding="utf-8") print("🔍 检查生成文件的语法...") if not check_syntax(INIT_FILE): print("❌ 语法检查失败,请检查生成的文件") return print("✅ 语法检查通过") print(f"✅ 已更新 {INIT_FILE}") print("🎉 更新完成!") print("\n📈 统计信息:") for module_name in MODULE_ORDER: print( f" {MODULE_LABELS[module_name]} 函数数: {len(inventory[module_name].functions)}" ) print(f" 总导出符号数: {len(target_symbols)}") print( f" 别名数: {len([name for name in operations_functions + evolve_functions if name in ALIAS_RULES])}" ) print("\n💡 建议:") print(f" 1. 检查生成的 {INIT_FILE} 文件") print(" 2. 运行测试确保所有导入正常工作") if args.backup: print(f" 3. 如有问题,可以从备份文件 {INIT_FILE}.bak 恢复") if __name__ == "__main__": main()