Ray 文档代码片段编写与 CI 自动化测试完整指南:doctest / testcode / literalinclude 三种示例格式全解析
【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.项目地址: https://gitcode.com/gh_mirrors/ra/ray
本文是 Ray 开源仓库中《How to write code snippets》(writing-code-snippets.md)的深度技术指南。它面向所有为 Ray 文档(docstring 或用户指南)贡献代码示例的开发者,系统讲解如何编写可开箱即跑、并在 CI 中被自动化执行的代码片段:三种示例格式(doctest-style、code-output-style、literalinclude)的语法与取舍、难以测试/输出不稳定的场景处理、GPU 示例的 Bazel 配置、本地验证方法,以及失败示例的三类根因诊断。读完本文,你将能写出与 Ray 官方文档同等质量、持续被 CI 守护的示例代码。
前提说明:本文示例的指令语法基于 reStructuredText(
.rst),与文档 ray-contribute 系列 保持一致;若使用 Markdown 编写,则采用 MyST 语法,可参考 MyST 官方文档中关于 directives 的说明。本仓库自 2.10.0 版本起新页面统一使用 MyST Markdown。
一、三种示例格式:定义与渲染效果
Ray 文档的示例分为三种类型:doctest-style(交互式会话风格)、code-output-style(普通代码 + 独立输出块)和literalinclude(从外部模块文件引用)。它们都会被 CI 执行,但语法与适用场景不同。
1. doctest-style 示例
doctest-style模拟 Python 交互式会话:代码行以>>>开头,预期输出紧随其后。在.rst中使用.. doctest::指令:
.. doctest:: >>> def is_even(x): ... return (x % 2) == 0 >>> is_even(0) True >>> is_even(1) False在 MyST Markdown 中渲染效果如下:
>>> def is_even(x): ... return (x % 2) == 0 >>> is_even(0) True >>> is_even(1) False编写 docstring 时的简化写法:如果你是在写 Python 模块/类的 docstring(而非文档页面),可以省略.. doctest::指令,直接写缩进的>>>块。pytest 的--doctest-modules会自动拾取,代码更简洁:
def is_even(x): """Return True if x is even. Example: >>> def is_even(x): ... return (x % 2) == 0 >>> is_even(0) True >>> is_even(1) False """ return (x % 2) == 0这种写法在仓库源码中随处可见,例如 python/ray/data/read_api.py 中range等数据读取 API 的 docstring:
>>> import ray >>> ds = ray.data.from_items([1, 2, 3, 4, 5]) >>> ds # doctest: +ELLIPSIS >>> ds.schema()2. code-output-style 示例
code-output-style由一对指令组成:.. testcode::存放普通 Python 代码,.. testoutput::存放该代码的标准输出(stdout):
.. testcode:: def is_even(x): return (x % 2) == 0 print(is_even(0)) print(is_even(1)) .. testoutput:: True False渲染效果:
def is_even(x): return (x % 2) == 0 print(is_even(0)) print(is_even(1))True False要点:testcode中的代码不依赖>>>提示符,适合较长、面向过程的多行代码;testoutput必须与testcode的实际 stdout逐字符一致(包括换行与空白),否则 CI 报错。
3. literalinclude 示例
literalinclude直接引用仓库中真实的.py模块文件,用:start-after:/:end-before:按标记截取片段,从源头上杜绝"文档代码与示例文件不一致":
.. literalinclude:: ./doc_code/example_module.py :language: python :start-after: __is_even_begin__ :end-before: __is_even_end__其引用的实际文件是 doc/source/ray-contribute/doc_code/example_module.py:
# example_module.py # fmt: off # __is_even_begin__ def is_even(x): return (x % 2) == 0 # __is_even_end__ # fmt: on渲染时只展示两个标记之间的代码:
def is_even(x): return (x % 2) == 0注意:
doc_code/目录下的.py文件本身也是 CI 测试对象(见下文"构建层面的测试规则"),因此 literalinclude 是"展示即测试"——读者看到的每一行代码都真实存在于仓库并被 CI 跑过。
二、如何选择示例类型
没有硬性规则,选择最能说明你 API 的风格即可。如果你不确定,指南给出的默认建议是:优先使用 code-output-style(testcode+testoutput),因为它对输出格式的约束最宽松。
什么场景用 doctest-style
当示例很短,且重点是展示对象的 repr 表示(比如打印中间对象、展示 schema 结构)时,用 doctest-style。例如展示ray.data.range的 schema 与take结果:
.. doctest:: >>> import ray >>> ds = ray.data.range(100) >>> ds.schema() Column Type ------ ---- id int64 >>> ds.take(5) [{'id': 0}, {'id': 1}, {'id': 2}, {'id': 3}, {'id': 4}]这类"对象表示即输出"的用例,>>>逐行对比的格式天然适合。
什么场景用 code-output-style
当示例较长,或对象的 repr 与示例主题无关时,用 code-output-style。典型如端到端的批处理变换(此例来自 Ray Data 文档,展示了map_batches的真实输出):
.. testcode:: from typing import Dict import numpy as np import ray ds = ray.data.read_csv("s3://anonymous@air-example-data/iris.csv") # Compute a "petal area" attribute. def transform_batch(batch: Dict[str, np.ndarray]) -> Dict[str, np.ndarray]: vec_a = batch["petal length (cm)"] vec_b = batch["petal width (cm)"] batch["petal area (cm^2)"] = np.round(vec_a * vec_b, 2) return batch transformed_ds = ds.map_batches(transform_batch) print(transformed_ds.materialize()) .. testoutput:: shape: (150, 6) ╭───────────────────┬──────────────────┬───────────────────┬──────────────────┬────────┬───────────────────╮ │ sepal length (cm) ┆ sepal width (cm) ┆ petal length (cm) ┆ petal width (cm) ┆ target ┆ petal area (cm^2) │ │ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │ │ double ┆ double ┆ double ┆ double ┆ int64 ┆ double │ ╞═══════════════════╪══════════════════╪═══════════════════╪══════════════════╪════════╪═══════════════════╡ │ 5.1 ┆ 3.5 ┆ 1.4 ┆ 0.2 ┆ 0 ┆ 0.28 │ │ 4.9 ┆ 3.0 ┆ 1.4 ┆ 0.2 ┆ 0 ┆ 0.28 │ │ 4.7 ┆ 3.2 ┆ 1.3 ┆ 0.2 ┆ 0 ┆ 0.26 │ │ 4.6 ┆ 3.1 ┆ 1.5 ┆ 0.2 ┆ 0 ┆ 0.3 │ │ 5.0 ┆ 3.6 ┆ 1.4 ┆ 0.2 ┆ 0 ┆ 0.28 │ │ … ┆ … ┆ … ┆ … ┆ … ┆ … │ │ 6.7 ┆ 3.0 ┆ 5.2 ┆ 2.3 ┆ 2 ┆ 11.96 │ │ 6.3 ┆ 2.5 ┆ 5.0 ┆ 1.9 ┆ 2 ┆ 9.5 │ │ 6.5 ┆ 3.0 ┆ 5.2 ┆ 2.0 ┆ 2 ┆ 10.4 │ │ 6.2 ┆ 3.4 ┆ 5.4 ┆ 2.3 ┆ 2 ┆ 12.42 │ │ 5.9 ┆ 3.0 ┆ 5.1 ┆ 1.8 ┆ 2 ┆ 9.18 │ ╰───────────────────┴──────────────────┴───────────────────┴──────────────────┴────────┴───────────────────╯ (Showing 10 of 150 rows)注意这里的表格输出来自 polars 风格的 repr;当输出格式随依赖库版本变化时,更稳妥的做法是配合下文第 4 节的省略号策略,只断言稳定片段。
什么场景用 literalinclude
当编写端到端示例、且示例本身不含输出时,用 literalinclude。它最适合"完整可运行脚本"场景:读者可以打开真实文件查看全貌,CI 直接执行该文件,双份维护成本为零。
三、难以测试的示例怎么办
何时允许不测试
依赖外部系统的示例可以不测试,例如需要 Weights & Biases 账号的集成示例。判断标准是:示例是否依赖网络、凭据或外部服务。
跳过 doctest-style 示例
在 Python 代码行尾追加# doctest: +SKIP即可跳过该行(该行代码不会被执行、其输出不会被校验):
.. doctest:: >>> import ray >>> ray.data.read_images("s3://private-bucket") # doctest: +SKIP+SKIP是 Python 标准 doctest 指令,+ELLIPSIS(允许...模糊匹配)等其它指令同样可用——read_api.py 中大量使用了这两种指令组合:
>>> ds = ray.data.read_images(path) # doctest: +SKIP >>> ds = ray.data.read_zarr( # doctest: +SKIP ... "s3://bucket/path.zarr", ... ... ... )跳过 code-output-style 示例
给testcode块加:skipif: True选项,整段代码将被跳过且不渲染输出断言:
.. testcode:: :skipif: True from ray.air.integrations.wandb import WandbLoggerCallback callback = WandbLoggerCallback( project="Optimization_Project", api_key_file=..., log_config=True )api_key_file=...需要替换为真实的凭据文件路径——正因为依赖外部服务,才需要用:skipif: True跳过 CI 执行,同时把代码留在文档中供用户参考。
四、长输出或非确定性输出怎么处理
当代码本身非确定(如随机数、时间戳、分布式对象地址),或输出过长时,有三种策略:省略号模糊匹配、模拟输出(MOCK)、完全省略输出块。
doctest-style:用省略号忽略部分输出
把不稳定的部分替换为...:
>>> import ray >>> ray.data.read_images("s3://anonymous@ray-example-data/image-datasets/simple") Dataset(num_rows=..., schema=...)num_rows、schema的具体内容被...替代,CI 只校验稳定的前缀与括号结构。需要说明的是:doctest 要启用...通配需依赖+ELLIPSIS指令(标准 doctest 默认关闭),实际项目中通常显式标注# doctest: +ELLIPSIS(见 read_api.py)。
要完全忽略输出(不展示也不校验),指南的明确建议是:改写为 code-output-style 并省略testoutput块,而不要使用# doctest: +SKIP——因为 SKIP 是给"依赖外部系统"的场景用的,滥用会掩盖真实的回归(详见第六节)。
code-output-style:三种输出策略
策略 A——省略号模糊匹配:把输出中长或不确定的部分替换为...:
.. testcode:: import ray ds = ray.data.read_images("s3://anonymous@ray-example-data/image-datasets/simple") print(ds) .. testoutput:: Dataset(num_rows=..., schema=...)策略 B——展示样例输出(MOCK):输出非确定、但你希望读者看到样例时,给testoutput加:options: +MOCK。此时 CI 不校验内容,页面仍展示样例:
.. testcode:: import random print(random.random()) .. testoutput:: :options: +MOCK 0.969461416250246策略 C——完全隐藏输出:输出难测且无需展示时,直接省略testoutput块,代码仍会被执行、但 stdout 不做断言:
.. testcode:: print("This output is hidden and untested")五、用 GPU 测试示例:Bazel doctest 规则配置
当示例需要 GPU(例如 Ray Data 的 GPU 批推理、Ray Train 的分布式训练)时,必须把它从默认的 CPU doctest 规则中排除,并加入独立的 GPU doctest 规则。操作分五步:
第 1 步:定位 BUILD 文件。示例位于doc/目录下则打开 doc/BUILD.bazel;示例位于 Python 库目录(如python/ray/train/)则打开对应的 python/ray/train/BUILD.bazel。
第 2 步:找到doctest规则。它形如(仓库中 doc/BUILD.bazel 的全局规则即如此):
doctest( files = glob( include=["source/**/*.rst"], ), size = "large", tags = ["team:none"] )第 3 步:把你的示例文件加入 exclude 列表,使其脱离 CPU 默认规则:
doctest( files = glob( include=["source/**/*.rst"], exclude=["source/data/requires-gpus.rst"] ), tags = ["team:none"] )第 4 步:新建(或复用)gpu = True的 doctest 规则:
doctest( files = [], tags = ["team:none"], gpu = True )第 5 步:把示例文件加入该 GPU 规则,并视需要设置size:
doctest( files = ["source/data/requires-gpus.rst"] size = "large", tags = ["team:none"], gpu = True )仓库中的真实对照:在 doc/BUILD.bazel 中可以同时看到 CPU 与 GPU 规则的完整形态。doctest_each宏为 data 库的每个文档单独建一个测试目标,并把batch_inference.rst、transforming-data.rst从 CPU 规则中排除后单独放进doctest[data-gpu]:
doctest_each( files = glob( include = ["source/data/**/*.md", "source/data/**/*.rst"], exclude = [ "source/data/batch_inference.rst", "source/data/transforming-data.rst", "source/data/api/**/*.rst", ], ), pytest_plugin_file = "//python/ray/data:tests/doctest_pytest_plugin.py", tags = ["team:data"], ) doctest( name = "doctest[data-gpu]", files = [ "source/data/batch_inference.rst", "source/data/transforming-data.rst", ], gpu = True, pytest_plugin_file = "//python/ray/data:tests/doctest_pytest_plugin.py", tags = ["team:data"], )Python 库侧同理,python/ray/train/BUILD.bazel 中py_doctest[train]排除了 GPU 相关文件,py_doctest[train-gpu]则用gpu = True单独承接:
doctest( name = "py_doctest[train]", size = "large", env = {"RAY_TRAIN_V2_ENABLED": "1", "TF_USE_LEGACY_KERAS": "1"}, files = glob( ["**/*.py"], exclude = [ "examples/**", "tests/**", "horovod/**", "mosaic/**", "tensorflow/tensorflow_trainer.py", "_internal/session.py", "context.py", ], ), tags = ["team:ml"], ) doctest( name = "py_doctest[train-gpu]", size = "large", env = {"RAY_TRAIN_V2_ENABLED": "0"}, files = ["_internal/session.py", "context.py", "tensorflow/tensorflow_trainer.py"], gpu = True, tags = ["team:ml"], )底层原理:doctest宏定义在 bazel/python.bzl 中。当gpu = True时,宏会把规则名追加[gpu]后缀、给标签追加gputag(否则追加cpu),并最终生成一个py_test目标,其 pytest 参数为:
--doctest-modules:拾取 docstring 中的>>>示例;--doctest-glob='*.md':额外拾取 Markdown/MyST 文档中的testcode块;--disable-warnings、-v;-c NO_PYTEST_CONFIG:避免全局 pytest.ini 干扰 doctest;-p <pytest_plugin_file>:注入仓库自研的 pytest 插件(默认是 bazel/default_doctest_pytest_plugin.py,Ray Data 使用自己的 python/ray/data/tests/doctest_pytest_plugin.py)。
这些插件为示例执行提供确定性环境:默认插件注册了 module 级别的ray.shutdown()fixture 保证测试间状态隔离;Data 插件还把RAY_DATA_PARQUET_FOOTER_NUM_ACTORS设为 1(避免 32 个 actor 的默认 footer 读取池触发 Ray 的"worker 进程过多"告警,从而污染testoutput断言)、固定preserve_order = True、关闭执行启动横幅——这也是文档示例输出必须可复现的原因:CI 已为输出确定性做了大量铺垫。
六、本地验证:pytest-sphinx 与 pytest --doctest-modules
CI 只是最后一道关,提交 PR 前应先在本地跑通示例。Ray 使用自维护的pytest-sphinxfork(正是它把.. testcode::/.. testoutput::翻译成 pytest 可执行的测试):
pip install git+https://github.com/ray-project/pytest-sphinx然后对模块、docstring 或用户指南分别运行 pytest:
# 测试整个模块的 docstring 示例 pytest --doctest-modules python/ray/data/read_api.py # 只测试某个函数/类的 docstring 示例 pytest --doctest-modules python/ray/data/read_api.py::ray.data.read_api.range # 测试文档页面(.rst)中的 testcode/doctest 块 pytest --doctest-modules doc/source/data/getting-started.rst注意第三条命令的路径映射:doc/source/data/getting-started.rst正是仓库 doc/source/data 目录下的真实文件;运行前需确保本地已安装 Ray(pip install -e .或使用编译好的 wheel)以及示例所需的第三方依赖。
七、调试失败的示例:两类问题、三种意图
CI 中示例失败时,先回答两个问题:这是什么类型的失败?这个示例原本在保护什么?
第一步:判断失败类型
| 失败类型 | 表现 | 处理方式 |
|---|---|---|
| 输出不匹配 | 示例运行成功,但 stdout 与testoutput或>>>预期不符 | 普通测试失败。本地pytest --doctest-modules <file>复现,对比实际输出与预期块:要么代码行为变了,要么预期输出写错了,修正其一 |
| 构建期失败 | 示例还没运行,构建就中止(import 错误、conf.py报错) | 先修复构建错误,再重读日志——中止之后的日志不可靠 |
| Sphinx 警告 | 渲染网关将警告视为错误导致构建失败 | 文档站点宿主(Read the Docs)的渲染门禁将警告视为错误,指令格式错误或交叉引用失效都会使构建失败,即使代码本身正确。这是标记语言问题而非代码问题,参见 Read the Docs render gate |
第二步:判断示例的保护意图
失败示例是一个信号,正确应对取决于它原本要防什么:
1. 破坏性变更探测器(breaking-change detector)。片段是用户可见代码,因库行为变更而失效。此时应把失败当作真实信号:要么不放行该变更,要么带着破坏性变更通知发布。变更获批后,必须同步更新示例使其匹配新行为,保证页面与随版本发布的内容一致。真正错误的是"只改示例不告知变更"——那会向复制了旧示例的用户隐藏变更。
2. 示例校验器(example validator)。示例本身错了:笔误、坏合并、过期 import。直接修复示例即可,不要误判为代码回归。
3. 漂移指示器(drift indicator)。示例仍能运行,但其周围的叙述文字已与代码实际行为脱节。此时更新叙述,而不只是示例。
同一个示例在不同时期可能以不同方式失败:坏合并引入的IndentationError属于示例校验器失败,修片段;上游 API 变更导致的ImportError则是破坏性变更信号,要修代码或对外沟通变更。失败文本会告诉你属于哪种情况。
关于跳过与 docs-go 标签的边界
让意图指导是否跳过:# doctest: +SKIP或:skipif: True只适用于依赖外部系统的示例;用它们应对破坏性变更探测器是错误的——那会把真实破坏从用户眼前藏起来。
同样的推理适用于docs-go标签:它跳过整个 PR 的各库示例测试步骤,是给"纯叙述改动、不触碰任何示例"的 PR 提供的便利通道,不是绕过红色示例测试的捷径。标签由守卫步骤lint: validate docs-go scope约束,仅当 PR 改动全部落在文档内容(doc/下的.md/.rst与图片、仓库根的.vale.ini/.vale/、或 API 一致性检查器源码ci/ray_ci/doc/)时才有效,且需要写权限才能添加。
八、CI 中的完整链路:从 PR 到合入
理解上述规则后,可以串起 Ray 文档 CI 的完整视图(详见 CI 测试工作流):
- 按路径路由:文档示例测试按库路由——改动
doc/source/ray-core/、ray-observability/跑core: docs example tests;data/、ray-more-libs/跑data: docs example tests;train/、tune/、ray-air/跑ml: docs example tests;rllib/、serve/各有专属步骤。这些步骤执行的就是本文所述的doctest、testcode、literalinclude片段(Per-library docs example tests)。 - 渲染门禁:Read the Docs 以
fail_on_warning: true构建站点,任何 Sphinx 警告都会失败,所以指令格式必须正确。 - 纯叙述改动:只改
.md/.rst/图片的 PR 不触发任何库测试步骤;可执行资产(.py/.ipynb)与示例消费的配置资产(.yaml/.sh)仍会路由到所属库。 - 合入门禁:合入前完整测试套件必须通过;外部贡献者的 PR 需 committer 添加
go标签触发全量测试。
九、仓库实践速查
- 三种格式的规范定义:writing-code-snippets.md
- CI 路由与 docs-go 标签:ci.md
doctest/doctest_each宏实现:bazel/python.bzl- 文档侧 doctest 规则(含 contenteditable="false">【免费下载链接】rayRay is an AI compute engine. Ray consists of a core distributed runtime and a set of AI Libraries for accelerating ML workloads.
项目地址: https://gitcode.com/gh_mirrors/ra/ray
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考