news 2026/9/10 14:16:07

FreeCAD Python API 弃用清单工具(python_api)使用与原理深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FreeCAD Python API 弃用清单工具(python_api)使用与原理深度解析

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 索引,并提供checkmanifestlist三个 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.precisionPart.Face.Wire以及DraftUtils.readDXFFreeCADGui._MDIView.messagePart.insertPart.openPathApp.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.2

2.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_kindmessage这类运行时字段。

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_apiFreeCADGui.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.pyiFreeCADGui
  • src/Mod/<Workbench>/App/...下的文件取App之后的路径,如src/Mod/Part/App/Thing.pyiPart.Thing
  • src/Appsrc/Basesrc/Gui下的文件去掉首层目录,如src/App/Example.pyExample
  • 普通__init__.py/__init__.pyi只贡献目录路径,其余文件名去掉后缀参与拼接。

测试 test_workbench_module_stubs_use_public_module_names 验证了PathAppPart这类工作台公开模块名的正确推导。

四、元数据约定:@deprecated 与 @deprecated_attributes

4.1 结构化生命周期元数据

README 强调"结构化生命周期元数据会被校验"(Structured lifecycle metadata is validated)。所有@deprecated装饰器必须使用关键字参数形式,合法字段恰好四个(STRUCTURED_FIELDS):

字段必填类型说明
deprecated_instr开始弃用的版本,须匹配^\d+\.\d+(\.\d+)?$(如26.3
removed_instr计划移除的版本,必须晚于deprecated_in
replacementstr 或 None替代 API 的调用建议(如new_api()
detailsstr 或 None附加说明文本

运行时装饰器实现在 src/Ext/freecad/deprecation.py:_validate_lifecycle对空字符串、非法版本格式(如"next")以及removed_in <= deprecated_in均抛出ValueErrorreplacement/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_inremoved_in must be later than deprecated_in
  • 非字面量/非字符串字段deprecated_inremoved_in必须是非空字符串,replacementdetails必须是字符串或 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 解析PythonNameName两种键,缺省回退到绑定模块.类名。该逻辑仅在.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_apiExample.older_apiFreeCADGui.hidePart.ThingPart.Thing.old_attrPart.Thing.old_method

七、数据模型与 CLI 退出码速查

7.1 数据模型

model.py 定义了三个冻结数据类:

  • DeprecationRecordsymbolkinddeprecated_inremoved_inreplacementdetailssource(仓库相对路径)、line
  • Diagnosticsourcelineseverity(error 级决定退出码)、message
  • ScanResultrecordsdiagnostics的只读元组。

7.2 退出码约定

退出码场景
0check/manifest/list正常完成(manifest/list即使有 error 诊断也返回 0,仅check与错误数绑定)
1check发现至少一条 error 级诊断
2list --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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 14:14:36

ZeroTierOne Android SDK 集成:6 步把跨网设备拉进同一个虚拟局域网

ZeroTierOne Android SDK 集成&#xff1a;6 步把跨网设备拉进同一个虚拟局域网 【免费下载链接】ZeroTierOne A Smart Ethernet Switch for Earth 项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOne 你的笔记本在办公室内网&#xff0c;手机在咖啡店 Wi-F…

作者头像 李华