619 lines
19 KiB
Python
619 lines
19 KiB
Python
#!/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()
|