Ansible 公开 API 边界设计:读懂 context/public-api.md 中的 sunder 前缀与 _internal 包约定
本文以 context/public-api.md 为核心,系统讲解 ansible-core 2.19 起"刻意管理公共 API 表面"的设计原则:为什么公开 Python 模块要求使用 sunder 前缀(_xxx)导入、为什么新实现默认放入 ansible._internal 包、以及这些约定在当前仓库源码中的真实落地形态。读完后你将能准确判断一段 ansible 代码属于"可依赖的公开 API"还是"随时可能改动的内部实现",并在自己的项目中复用同一套边界划分方法。
一、背景:从 ansible-core 2.19 开始"刻意化"的公共表面
context/public-api.md 开篇即给出总纲:
Starting with ansible-core 2.19 we're trying to be more intentional about what features are part of our public API and other public surface areas (e.g. CLI, configuration options, module arguments, ansible-core provided Jinja globals, etc.).
这意味着 Ansible 团队从 2.19 版本开始,把"什么是承诺给外部消费者的 API"当成一等工程问题来对待。文档明确列出的"公共表面"(public surface area)不局限于 Python 符号,还包括:
- CLI:
ansible-playbook、ansible-config、ansible-galaxy等命令行的参数与子命令(对应 lib/ansible/cli/ 下的实现); - 配置选项:
ansible.cfg与环境变量配置项(定义于 lib/ansible/config/base.yml); - 模块参数:各模块
argument_spec中暴露给 playbook 作者的参数; - ansible-core 提供的 Jinja 全局变量:模板里可以直接使用的内置全局。
这一总纲回答了一个关键问题:当你 import 或调用 ansible 代码时,哪些名称是"契约",哪些只是"内部脚手架"。 下面两节分别给出文档定义的两条落地规则。
二、规则一:公开 Python 模块内的导入必须 sunder 前缀化
原文档"Imports in public Python modules"一节给出了两条硬性规范,其目的是让公开模块"更自文档化"(more self-documenting):
1. 导入名必须带 sunder 前缀
Imports in any file considered public API must be sunder-prefixed (e.g.
_module_name), to avoid confusion about imported objects being part of the public module API.
任何被视为公开 API 的文件,其中所有 import 出来的名字都必须以下划线开头(例如 import os as _os)。这样做的动机是消除歧义:如果一个模块里出现 from somewhere import Foo,使用者无法判断 Foo 是该模块"重新导出"的公开 API,还是仅仅被内部使用恰好没加前缀的私有依赖。统一 sunder 化后,一个公开模块里所有不带下划线的名字就都只能是有意公开的 API。
这条规范在 lib/ansible/template/init.py 中可以得到完整印证,该文件是公开模块,其导入区几乎全部 sunder 化:
import contextlib as _contextlib
import io as _io
import os as _os
import typing as _t
from jinja2 import environment as _environment
from ansible import _internal
from ansible import errors as _errors
from ansible._internal._datatag import _tags, _wrappers
from ansible._internal._templating import _jinja_bits, _engine, _jinja_common, _template_vars
from ansible.module_utils import datatag as _module_utils_datatag
from ansible.utils.display import Display as _Display
连第三方库 jinja2 的 environment 和标准库 contextlib 都被重命名为 _environment、_contextlib——这正是"import 名字绝不与公开 API 混淆"的字面体现。
2. 优先模块级导入 + 点号用法
Prefer module-level imports (e.g.
from ansible._internal import _amodule) with dotted usage (e.g.foo: _amodule.thing). This solves many circular import issues and reduces the need for sunder-prefix aliasing on internal imports in public API.
即推荐"导入模块而非导入名字"的写法:from ansible._internal import _task,使用时写成 _task.Task 而不是 from ansible._internal._task import Task as _Task。文档指出这有两个直接收益:
- 缓解循环导入:导入模块对象发生在包加载完成之后,点号访问推迟到运行期,打破了"A 导入 B 的名字、B 又导入 A"的紧耦合环;
- 减少别名数量:不必为每个内部名字都造一个
_Xxx别名,公开模块的导入区更短、可读性更好。
仓库中 lib/ansible/executor/process/worker.py 第 32 行就是标准示范:
from ansible._internal import _task
lib/ansible/executor/task_executor.py、lib/ansible/cli/config.py、lib/ansible/executor/module_common.py 等公开执行器模块也都采用同一模式(from ansible._internal import _task / _json / _locking / _ansiballz),说明这已不是个别文件的选择,而是全仓库统一执行的规范。
文档同时留了一个务实的口子:
Hot code paths can use locals or aliased objects, but sparingly and only where it really matters.
热点路径(例如被反复调用的循环体内)可以把 _amodule.thing 缓存到局部变量或局部别名以避免重复属性查找,但必须"克制、只在真正重要的地方"。这体现了规范为性能预留了逃逸阀,却不允许滥用。
三、规则二:新实现"默认内部化",全部落到 _internal 包
原文档"Internal by default"一节规定:Python 中新功能的实现应当总是满足——
- 加在
ansible._internal或ansible.module_utils._internal这两个包之下; - 使用 sunder 前缀的模块名(这样公开模块导入它们时天然带下划线,无需额外别名)。
并给出两条边界说明:
- 只有公开类型和公开函数才允许出现在
_internal包之外的新模块里; - 公开模块中可以放"小型 sunder 前缀工具类型/函数",但绝大多数非公开 API 的实现应完全活在
_internal包内; _internal包内部的类型/函数/方法一般不需要再逐个加 sunder 前缀——因为包路径本身已经声明了"内部"身份。
当前仓库中这两个包都已实际成形。lib/ansible/_internal/ 目录(见 lib/ansible/_internal/)包含了 _task.py、_display_utils.py、_event_formatting.py、_collection_proxy.py、_locking.py、_rpc_host.py 以及 _templating/、_errors/、_json/、_datatag/、_ssh/、_powershell/ 等成组子包;lib/ansible/module_utils/_internal/ 下则是模块侧的 _validation.py、_deprecator.py、_errors.py、_json 等实现。注意包内部的名字(如 _task.py 中的 Task 相关类)确实普遍不再逐符号加前缀,与文档"internal-only 名字不必 sunder"的说明一致。
两个包的分工也很清晰:
ansible._internal(控制器侧):playbook 执行引擎、模板引擎、错误体系、RPC worker 等只运行在控制节点(运行 ansible 的机器)上的实现;ansible.module_utils._internal(模块侧):会被打包进 Ansiballz 模块、随模块一起发到远端执行的共享实现。lib/ansible/module_utils/_internal/init.py 顶部就定义了序列化/递归用的公共常量,如INTERMEDIATE_MAPPING_TYPES、INTERMEDIATE_ITERABLE_TYPES、ITERABLE_SCALARS_NOT_TO_ITERATE,供模块侧序列化管线递归转换使用。
公开包入口里的"逃生舱"设计
lib/ansible/_internal/init.py 展示了"内部包如何受控地暴露极少数公开入口"的手法。该文件提供:
experimental装饰器:文档注释写明,它标记"位于_internal包之外、却接收或暴露内部类型"的实验性类型与方法,声明与内部 API 一样随时可能无预告变更——为极少数无法完全躲进_internal的公开符号给出了显式的"不承诺稳定"标签;import_controller_module与get_controller_serialize_map:注入到 module_utils 代码中、在控制器上下文中替换 no-op 版本的可调用对象,是模块侧与控制器侧共用代码的衔接点。
这类设计让"内部实现默认不外泄"的原则有了工程出口:确需跨界时,要么走受控注入,要么打上 experimental 免责标记,而不是悄悄扩大公开 API。
四、配套的代码风格与格式约束
这些 API 边界约定并非孤立存在,而是与仓库其他工程规范互相咬合:
- context/coding-style.md 明确要求"被视为公开 API 的任何代码都必须有 docstring"——公开面越大,文档义务越重,这反向激励贡献者把非公开实现藏进
_internal; - 同文还说明:
black校验针对所有_internal包以放宽的 160 行宽运行、且不做引号转换,即内部实现区在格式化工具链上也是被单独对待、单独约束的,与公开代码区划出边界。
从 lib/ansible/executor/process/worker.py 还能看到另一处呼应:公开模块用 __all__ = ['WorkerProcess'] 显式声明唯一对外名字,配合 sunder 前缀导入,使"公开面"在静态层面完全可枚举。
五、这套约定的工程收益与适用前提
综合 context/public-api.md 的原文与仓库源码证据,这套"公开 API 边界工程"带来三点可迁移的收益:
- 可枚举的公开契约:公开模块里不带下划线的顶层名字、
__all__、以及_internal包之外的新模块,共同构成可被工具枚举的 API 清单,发版时可以做破坏性变更检测; - 内部实现获得"任意重构权":因为
_internal下的符号被明确声明不受兼容承诺保护,团队可以激进重构(本仓库 2.19 起的大规模重命名、_wrapt、_templating子包拆分即受益于此); - 循环导入与别名噪音下降:
from ansible._internal import _task这类模块级点号用法,在 lib/ansible/executor/、lib/ansible/cli/ 等公开模块中普遍生效,使执行器、CLI 与内部实现之间的依赖关系更松散。
适用前提同样需要说清:这些规范以 ansible-core 2.19 为起点(文档首句已注明),面向的是"被第三方 Python 代码 import 或扩展"的消费场景;如果你只是写 playbook 使用 CLI/模块参数,受影响的边界是文档第一节列出的 CLI、配置、模块参数与 Jinja 全局,而非 Python 符号本身。对贡献者而言,判断新代码该放哪里的流程就是:先问"它是公开 API 吗?"——不是,就进 _internal 包并起 sunder 模块名;是,就放公开模块、加 docstring,并让其内部导入全部 sunder 前缀。
六、延伸阅读路径
- 规范原文:context/public-api.md
- 公开模块 sunder 导入完整实例:lib/ansible/template/init.py
- 公开模块的模块级内部导入实例:lib/ansible/executor/process/worker.py、lib/ansible/executor/module_common.py
- 内部包入口与
experimental标记:lib/ansible/_internal/init.py - 模块侧内部包与序列化常量:lib/ansible/module_utils/_internal/init.py
- 公开 API 的 docstring 义务与 black 策略:context/coding-style.md
掌握以上两条规则(公开模块导入 sunder 化 + 新实现默认 _internal),你就能在任何一份 ansible 源码文件里快速识别出"稳定契约区"和"自由变更区",这正是阅读与扩展 ansible-core 代码库的基本功。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00