FreeCAD Python API 弃用清单工具(python_api)使用与原理深度解析
【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD
导读
FreeCAD 是一个跨平台的开源参数化 3D 建模软件,其庞大的 Python API 面向脚本编写者和二次开发者。随着版本迭代,API 必然经历废弃与淘汰,而手动维护一份准确的弃用清单既不现实也容易出错。src/Tools/python_api/README.md 描述了一套直接从 FreeCAD 源码中自动发现 Python API 弃用信息的工具:它基于源码装饰器(decorator)扫描整个仓库,输出确定性的 JSON 索引,并提供check、manifest、list三个 CLI 子命令。本文将以该 README 为主线,结合其源码实现(cli.py、deprecations.py、model.py)与测试用例(test_python_api_deprecations.py),完整讲解工具的使用方式、元数据约定、严格校验规则与底层实现原理。读完本文,你将能运行该工具生成仓库的弃用索引、按版本筛选待移除 API,并理解其"静态扫描、零导入、强校验"的设计哲学。
一、工具定位与设计原则
1.1 从源码中发现弃用信息,而非手写清单
README 开篇即点明工具的核心定位:直接(directly)从 FreeCAD 源文件中发现 Python API 弃用信息。这意味着:
- 源码装饰器是权威(authoritative):一处标记,处处生效,杜绝了清单与源码不同步的问题;
- JSON 输出只是确定性的索引(deterministic index):README 明确说明 JSON 输出"not intended to be edited or committed",即产物不应被人工修改或提交进仓库,它只是供下游消费的快照。
1.2 边界明确:不做什么
README 在结尾明确划定了工具的功能边界:Git 历史、API 签名跟踪、跨版本存储(cross-release storage)刻意不在其范围内。从源码看,这一边界是真实的——scan_repository 只做当前工作区的一次性快照扫描,没有任何依赖 Git 历史或版本数据库的逻辑。读者应据此判断该工具的适用场景:它是一把"当下状态检查器",而非"历史迁移分析器"。
1.3 测试保障
README 声称"仓库扫描由 src/Tools 测试套件覆盖",这在 test_repository_scan_has_no_errors 中得到验证:该测试对真实仓库根目录执行scan_repository,断言零错误,并抽查了DraftVecUtils.precision、Part.Face.Wire以及DraftUtils.readDXF、FreeCADGui._MDIView.message、Part.insert、Part.open、PathApp.fromShape等真实存在的弃用记录。
二、快速上手:三个 CLI 子命令
README 给出四种典型用法(均需在仓库根目录执行),对应 cli.py 中定义的子命令解析逻辑:
# 校验全仓库的弃用元数据,打印诊断信息并统计 python -m src.Tools.python_api check # 生成确定性 JSON 清单,写入指定文件 python -m src.Tools.python_api manifest --output python-api-deprecations.json # 列出全部弃用记录(表格形式) python -m src.Tools.python_api list # 按"移除版本"筛选:只列出将在 27.2 及更早版本中被移除的 API python -m src.Tools.python_api list --remove-by 27.22.1 check:校验元数据质量
check遍历仓库,打印全部诊断(diagnostics),最后输出统计:
Found <N> deprecations and <M> errors.退出码与错误数绑定:有 error 级诊断时返回 1,否则返回 0(见 main 的return 1 if errors else 0),因此它天然适合接入 CI 门禁。
2.2 manifest:生成确定性 JSON 索引
manifest把扫描结果序列化为缩进 2、按键排序(sort_keys=True)的 JSON:
{ "schema_version": 1, "deprecations": [ { "symbol": "...", "kind": "...", "deprecated_in": "...", "removed_in": "...", "replacement": "...", "details": null, "source": "...", "line": 42 } ] }--output缺省为-(输出到 stdout);指定文件路径时,_write_manifest 会自动创建父目录(测试 test_manifest_creates_parent_directories 验证了这一点)。- 确定性由两方面保证:记录按
(symbol, kind, deprecated_in, removed_in, replacement, details, source, line)排序(_record_sort_key),且多条相同元数据只保留行号最小的一条;测试 test_manifest_is_deterministic 对同一仓库两次生成并断言 JSON 完全相等,同时确认输出中不包含metadata_kind、message这类运行时字段。
2.3 list:人类可读的表格
list默认以表格输出,表头为:
Symbol Deprecated Removal Replacement每条记录打印symbol / deprecated_in / removed_in / replacement(无替代方案时 replacement 显示-),空结果打印No matching deprecations.。
--remove-by 27.2:筛选removed_in <= 27.2的记录(_release_key 做版本归一化比较);若版本号不匹配^\d+\.\d+(\.\d+)?$格式,命令返回退出码 2 并向 stderr 报错invalid release: ...。--format json:以asdict输出完整记录 JSON。- 测试 test_list_filters_by_removal_release 展示了
--remove-by 27.1无匹配、--remove-by 27.2命中Example.old_api与FreeCADGui.hide的行为差异。
2.4 通用选项
三个子命令都接受--root指定仓库根目录(默认Path.cwd())。这意味着你可以在任意目录下对指定仓库副本执行扫描,例如:
python -m src.Tools.python_api --root /path/to/freecad check入口链为 __main__.py:raise SystemExit(main())将返回值直接作为进程退出码。
三、扫描范围与文件类型
3.1 扫描三类文件,且不导入模块
README 明确:扫描器读取常规 Python(.py)、绑定.pyi、以及.module.pyi文件,且不会导入(importing)它们。从源码看,scan_repository 对src下每个.py/.pyi文件调用ast.parse构建语法树——纯静态分析,不执行任何代码,因此扫描对构建环境零依赖,速度也更快。
3.2 排除规则
_source_paths 中通过SKIPPED_PARTS排除以下目录(出现在相对路径任意层级即跳过):
3rdParty SCL_output generated tests __pycache__另外,src/Tools自身(rel.parts[0] == "Tools")也被排除,避免工具扫描自身造成循环噪声。这也解释了为何测试目录src/Tools/tests/中的弃用示例不会污染真实仓库清单。
3.3 模块名推导规则
_module_name 决定每条记录的symbol前缀:
.module.pyi文件直接以文件名(去掉.module.pyi后缀)作为模块名,如src/Gui/FreeCADGui.module.pyi→FreeCADGui;src/Mod/<Workbench>/App/...下的文件取App之后的路径,如src/Mod/Part/App/Thing.pyi→Part.Thing;src/App、src/Base、src/Gui下的文件去掉首层目录,如src/App/Example.py→Example;- 普通
__init__.py/__init__.pyi只贡献目录路径,其余文件名去掉后缀参与拼接。
测试 test_workbench_module_stubs_use_public_module_names 验证了PathApp、Part这类工作台公开模块名的正确推导。
四、元数据约定:@deprecated 与 @deprecated_attributes
4.1 结构化生命周期元数据
README 强调"结构化生命周期元数据会被校验"(Structured lifecycle metadata is validated)。所有@deprecated装饰器必须使用关键字参数形式,合法字段恰好四个(STRUCTURED_FIELDS):
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
deprecated_in | 是 | str | 开始弃用的版本,须匹配^\d+\.\d+(\.\d+)?$(如26.3) |
removed_in | 是 | str | 计划移除的版本,必须晚于deprecated_in |
replacement | 否 | str 或 None | 替代 API 的调用建议(如new_api()) |
details | 否 | str 或 None | 附加说明文本 |
运行时装饰器实现在 src/Ext/freecad/deprecation.py:_validate_lifecycle对空字符串、非法版本格式(如"next")以及removed_in <= deprecated_in均抛出ValueError;replacement/details非字符串时抛TypeError。其生成的弃用警告消息格式为:
<module>.<name> is deprecated since FreeCAD <deprecated_in> and will be removed in FreeCAD <removed_in>; use <replacement> instead; <details>.测试 test_decorator_warns_with_lifecycle_and_replacement 验证了DeprecationWarning类别、消息内容以及__deprecated__属性的一致性;test_message_does_not_duplicate_terminal_punctuation 则保证句尾标点不重复。
4.2 运行时行为与静态索引的关系
值得注意:deprecated装饰器在运行时通过typing_extensions.deprecated(PEP 702)实现真正的DeprecationWarning触发,并借助_WRAPPER_CODES集合与_unwrap_deprecated_frame让警告堆栈直接指向调用者而非装饰器包装层。而src/Tools/python_api的扫描器只做语法层面的发现与校验,两者互为印证:装饰器是运行时真相,扫描器是静态索引。
4.3 类级属性弃用:deprecated_attributes
属性弃用通过类装饰器@deprecated_attributes声明,其参数是属性名到结构化元数据映射(_scan_attributes):
from Base.Metadata import deprecated, deprecated_attributes, export @export(PythonName="Part.Thing") @deprecated_attributes( old_attr={ "deprecated_in": "26.3", "removed_in": "27.2", "replacement": "new_attr", }, ) class ThingSpec: old_attr: int每个属性值必须是字典(结构化映射),非字典会被报错metadata must be a structured mapping;每个属性的 symbol 为类符号.属性名,kind 记为attribute。测试 fixture 中的Part.Thing.old_attr正是此路径的覆盖样例。
五、严格校验:哪些写法会被拒绝
README 点名的三类"拒绝为错误"(rejected as errors)均有对应实现与测试:
5.1 位置参数形式的 PEP 702 装饰器
from typing_extensions import deprecated @deprecated("Use replacement instead.") def old_function(): ..._scan_decorators 检测到@deprecated(...)带位置参数(decorator.args非空)即报错deprecated() requires structured keyword-only lifecycle metadata(测试 test_rejects_legacy_deprecation_metadata)。
5.2 字符串形式的 deprecated_attributes 元数据
@deprecated_attributes(old_attr="some string")这类非映射值会被判定为错误(见 4.3 节)。更一般地,_literal_keywords要求所有参数值可被ast.literal_eval求值:关键字解包(**kwargs)报keyword unpacking is not supported,非常量表达式报'<field>' must be a literal value。
5.3 仅存在于 docstring 的弃用说明
def old_docstring(): """Deprecated -- use replacement instead.""" ..._scan_docstring 用LEGACY_DOC_RE(大小写不敏感匹配deprecated后跟:/--/-)扫描函数、方法、类的 docstring,命中即报错<symbol> uses docstring-only deprecation metadata。注意:该规则并非禁止 docstring 中出现"Deprecated"字样,而是禁止以 docstring 作为唯一的弃用声明途径。
5.4 其他元数据一致性问题
- 未知字段:如
deadline="28.1"会报unknown deprecation field 'deadline'(test_reports_invalid_lifecycle_metadata); - 版本倒挂:
removed_in <= deprecated_in报removed_in must be later than deprecated_in; - 非字面量/非字符串字段:
deprecated_in、removed_in必须是非空字符串,replacement、details必须是字符串或 null; - 同名符号的冲突元数据:同一 symbol 若出现多套不同的
(deprecated_in, removed_in, replacement, details)生命周期组合,scan_repository 会报conflicting metadata for <symbol>; - 语法解析失败:无法
ast.parse的文件报cannot parse source: ...。
六、真实仓库中的使用样例
6.1 符号命名与导出
- 绑定类(binding classes)的公开名若与原生包装名不同,必须用
@export(PythonName="...")声明(README 明确要求)。_class_public_name 解析PythonName与Name两种键,缺省回退到绑定模块.类名。该逻辑仅在.pyi(非.module.pyi)的顶层类上生效(见 visit_ClassDef),FreeCADGui/FreeCAD绑定模块的映射见 _binding_module。 .module.pyi中的函数属于模块级公开 API,如FreeCADGui.hide。
6.2 重载(overload)折叠
同一函数的多个@overload变体若携带完全相同的弃用元数据,会合并为一条记录(test_matching_overload_metadata_collapses_to_one_record),保证清单里每个 symbol 至多一条记录。
6.3 完整 fixture 示例
以下组合展示了kind的四种取值(function / method / class / attribute)以及.py、.pyi、.module.pyi三种文件的混用,来自测试夹具 _fixture:
# src/App/Example.py —— 模块级函数(kind=function) from freecad.deprecation import deprecated @deprecated(deprecated_in="26.3", removed_in="27.2", replacement="new_api()") def old_api(): ... # src/Mod/Part/App/Thing.pyi —— 类、方法、属性(kind=class/method/attribute) from Base.Metadata import deprecated, deprecated_attributes, export @export(PythonName="Part.Thing") @deprecated_attributes(old_attr={"deprecated_in": "26.3", "removed_in": "27.2", "replacement": "new_attr"}) @deprecated(deprecated_in="26.3", removed_in="28.1", replacement="BetterThing") class ThingSpec: old_attr: int @deprecated(deprecated_in="26.3", removed_in="27.2", replacement="new_method()") def old_method(self) -> None: ... # src/Gui/FreeCADGui.module.pyi —— 模块级 API(绑定模块) from Base.Metadata import deprecated @deprecated(deprecated_in="26.3", removed_in="27.2", replacement="hideObject") def hide() -> None: ...扫描结果 symbol 集合为:Example.old_api、Example.older_api、FreeCADGui.hide、Part.Thing、Part.Thing.old_attr、Part.Thing.old_method。
七、数据模型与 CLI 退出码速查
7.1 数据模型
model.py 定义了三个冻结数据类:
DeprecationRecord:symbol、kind、deprecated_in、removed_in、replacement、details、source(仓库相对路径)、line;Diagnostic:source、line、severity(error 级决定退出码)、message;ScanResult:records与diagnostics的只读元组。
7.2 退出码约定
| 退出码 | 场景 |
|---|---|
| 0 | check/manifest/list正常完成(manifest/list即使有 error 诊断也返回 0,仅check与错误数绑定) |
| 1 | check发现至少一条 error 级诊断 |
| 2 | list --remove-by传入非法版本号 |
八、总结与延伸阅读
src.Tools.python_api是 FreeCAD 将"Python API 弃用管理"工程化的落点:以源码装饰器为单一事实源,通过纯 AST 静态扫描生成确定性索引,用严格校验把元数据质量问题消灭在提交之前。它不追踪历史、不跨版本存储,但足以支撑"当前仓库弃用面盘点"与"某版本前移除 API 清单"这两类高频需求。
- 工具入口与 CLI:cli.py、__main__.py
- 扫描与校验核心:deprecations.py
- 数据模型:model.py
- 运行时装饰器(运行时警告的真实来源):deprecation.py
- 测试套件:test_python_api_deprecations.py、test_deprecation.py
若你正在维护 FreeCAD 的 Python 绑定或工作台模块,请遵循本文第四节的元数据约定标注弃用 API,并定期在 CI 中运行check,即可确保仓库的弃用索引始终权威、一致、可机器消费。
【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考