首页
/ Ansible 公开 API 边界设计:读懂 context/public-api.md 中的 sunder 前缀与 _internal 包约定

Ansible 公开 API 边界设计:读懂 context/public-api.md 中的 sunder 前缀与 _internal 包约定

2026-09-04 09:33:09作者:柯茵沙

本文以 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-playbookansible-configansible-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

连第三方库 jinja2environment 和标准库 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.pylib/ansible/cli/config.pylib/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 中新功能的实现应当总是满足——

  1. 加在 ansible._internalansible.module_utils._internal 这两个包之下;
  2. 使用 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_TYPESINTERMEDIATE_ITERABLE_TYPESITERABLE_SCALARS_NOT_TO_ITERATE,供模块侧序列化管线递归转换使用。

公开包入口里的"逃生舱"设计

lib/ansible/_internal/init.py 展示了"内部包如何受控地暴露极少数公开入口"的手法。该文件提供:

  • experimental 装饰器:文档注释写明,它标记"位于 _internal 包之外、却接收或暴露内部类型"的实验性类型与方法,声明与内部 API 一样随时可能无预告变更——为极少数无法完全躲进 _internal 的公开符号给出了显式的"不承诺稳定"标签;
  • import_controller_moduleget_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 边界工程"带来三点可迁移的收益:

  1. 可枚举的公开契约:公开模块里不带下划线的顶层名字、__all__、以及 _internal 包之外的新模块,共同构成可被工具枚举的 API 清单,发版时可以做破坏性变更检测;
  2. 内部实现获得"任意重构权":因为 _internal 下的符号被明确声明不受兼容承诺保护,团队可以激进重构(本仓库 2.19 起的大规模重命名、_wrapt_templating 子包拆分即受益于此);
  3. 循环导入与别名噪音下降: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 前缀。

六、延伸阅读路径

掌握以上两条规则(公开模块导入 sunder 化 + 新实现默认 _internal),你就能在任何一份 ansible 源码文件里快速识别出"稳定契约区"和"自由变更区",这正是阅读与扩展 ansible-core 代码库的基本功。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384