首页
/ Black 与 Doctest 格式化:为什么 Black 不碰 Docstring 里的代码,以及如何用 blackdoc / blacken-docs 补齐

Black 与 Doctest 格式化:为什么 Black 不碰 Docstring 里的代码,以及如何用 blackdoc / blacken-docs 补齐

2026-09-05 14:32:37作者:翟萌耘Ralph

本文基于 Black 官方集成文档 docs/integrations/doctest_formatting.md 展开,讲清三个问题:Black 对 docstring 中可执行代码(doctest)的处理边界及其底层原因、两个社区工具 blacken-docsblackdoc 各自支持的场景与示例、以及两者行为差异下如何在项目中做出稳妥的选型。读完后,你可以为自己的仓库规划一套"Black 管源码、专用工具管文档内 doctest"的完整格式化方案。

Black 的默认行为:不格式化 docstring 内的 doctest 代码

Black 官方文档开篇即明确了这一职责边界:

Black 会对 docstring 的"风格"做一些决定,但不会假设文档内容本身的结构。因此,docstring 或文档文件中的可执行 Python 代码(例如 doctest),不会被 Black 格式化。

这个"克制"是有意为之的。doctest 的语义依赖字符串的精确内容:>>> 提示符、缩进、输出文本的换行,任何一处在被"格式化"后都可能改变 doctest 的比对结果甚至执行语义。把文档内容当作不透明文本处理,是保证 Black 幂等性与安全性的前提。

从源码看 Black 对 docstring 到底做了什么

结合仓库源码可以精确理解 Black 在 docstring 上的行为范围:

  1. 入口是字符串叶节点访问visit_STRING 是所有字符串(含 docstring)的格式化入口。其中 is_docstring(leaf) 判断命中后,Black 会:

    • 若开启了 string_normalization,先做前缀归一化(normalize_string_prefixF/B 小写、去掉 u/U 前缀)和引号归一化(normalize_string_quotes 优先双引号、必要时增删反斜杠);
    • 剥离字符串前缀与外层三引号,得到 docstring 纯内容;
    • 对多行 docstring 调用 fix_multiline_docstring 重新对齐缩进(按 lines_with_leading_tabs_expanded 展开前导 tab 后,以最小缩进为基准整体平移,遵循 PEP 257 的 docstring 缩进处理规则);
    • 对首尾含引号或奇数个尾随反斜杠的内容补空格,防止转义问题。

    注意整个流程中,docstring 的内部内容只按"文本行"做缩进平移,绝不会被再次解析为 Python 代码——这正是 doctest 代码不会被触碰的源码级原因。

  2. 有一个明确的豁免点visit_STRING 中对包含"反斜杠+换行"的 docstring 直接跳过重排,注释说明这样做会改变 AST 表示。这再次体现了 Black 对字符串内部语义的保守态度。

  3. 行为可由开关控制:docstring 的引号/前缀归一化属于 string_normalization 能力的一部分,该字段在 Mode 中默认为 True,可通过命令行 --skip-string-normalization(见 src/black/init.py)关闭。但即使关闭该开关,Black 也不会开始格式化 docstring 里的 doctest 代码——这条边界是写死在行为设计中的,与开关无关。

换言之:Black 负责 docstring 作为"字符串"的风格(引号、前缀、缩进对齐),而 docstring 作为"文档"的内容(含其中嵌入的 doctest)完全交还给专门工具处理。

补齐方案的总览:blacken-docs 与 blackdoc

官方文档在给出上述边界后,列出了两个将 Black 格式化规则应用到 docstring 和文档文件中代码的工具,并附有一段重要提示:

注意:这些包之间已观察到一些不一致行为,因此官方不愿给出推荐。安装即自担风险。同时欢迎社区贡献更多 doctest 格式化工具的文档。

以下两节分别继承原文档中两个工具的能力说明与示例。

blacken-docs:面向文档文件的 doctest 格式化

blacken-docs 主要用于将 Black 格式化应用到文档文件(如 .rst.md.tex)中的代码。它支持以下场景:

1. Markdown / reStructuredText / LaTeX 文件中的 Python 代码块

blackdoc 相同,此处应用的是普通 Black 格式化,因此 Python 代码块内部的 doctest 不会被处理。

Markdown 形式:

```python
print("Hello world!")
```

reStructuredText 形式:

.. code-block:: python
    print("Hello world!")

LaTeX 形式(minted 环境):

\begin{minted}{python}
print("Hello world!")
\end{minted}

2. Markdown 与 reStructuredText 中 Pycon 代码块里的 doctest

代码块可以位于 .md.rst 文件中,也可以位于 Python 文件的 docstring 内部:

```python
>>> print("Hello world!")
```
.. code-block:: pycon
    >>> print("Hello world!")
def add_one(n: int) -> int:
    """
    Examples
    --------
    ```pycon
    >>> add_one(1) == 2
    ```
    """
    return n + 1

可以看出 blacken-docs 的定位是"文档侧":它识别文档语法(fenced code block、.. code-block::、minted)中的 Pycon 块,对块内 doctest 应用 Black 风格。

blackdoc:面向 Python 文件的 doctest 格式化

blackdoc 主要用于将 Black 格式化应用到 Python 文件中的 doctest,且不会格式化任何已被 Black 本身覆盖的文件内容——两者职责互补、不重叠。它支持以下场景:

1. Python 文件中的 doctest

def add_one(n: int) -> int:
    """
    Examples
    --------
    >>> add_one(1) == 2
    """
    return n + 1

这是最典型的使用场景:>>> 语句在 docstring 中,Black 主流程(如 visit_STRING 所实现的逻辑)不会触碰它们,而 blackdoc 会解析这些 doctest 并按 Black 风格重排。

2. Markdown / reStructuredText 文件中的 Python 代码块

在这些情况下应用的是普通 Black 格式化,即 Python 代码块内部的 doctest 不会被处理:

```python
print("Hello world!")
```
.. code-block:: python
    print("Hello world!")

两个工具的能力边界对比

将原文档的说明整理为对照表,便于检索与选型(表中"覆盖"指该工具会对相应位置应用 Black 风格):

位置 blacken-docs blackdoc
.md / .rst / .tex 中的普通 Python 代码块 普通 Black 格式化(块内 doctest 不处理) 普通 Black 格式化(仅 .md / .rst,块内 doctest 不处理)
.md / .rst 中 Pycon 代码块的 doctest 覆盖 不覆盖
Python 文件 docstring 中的 doctest 覆盖(含 docstring 内的 ```pycon 块形式) 覆盖(标准 >>> doctest 形式)
已被 Black 主流程覆盖的内容 不重复处理 不重复处理

需要强调的是原文档的告诫:两个包之间存在已观察到的不一致行为,官方因此不做推荐,任何安装均自担风险。从源码结构看,这一谨慎态度与 Black 自身的设计哲学一致——Black 对字符串内容(含 doctest)保持"不透明处理"(见 src/black/strings.py),而 doctest 解析、Pycon 块识别等能力属于文档侧工具的独立实现,彼此之间并无共享的解析器,行为差异在所难免。

实践建议:分层规划格式化职责

基于本文的边界说明,一个稳妥的项目实践是:

  1. 源码层:所有 .py 文件交给 Black 本身(--check --diff 可用于 CI 校验),docstring 的缩进对齐、引号风格由 Black 自动处理;
  2. Python 文件中的 doctest:引入 blackdoc,只处理 >>> 形式的 doctest,不与 Black 主流程重叠;
  3. 文档文件(.md / .rst / .tex)中的代码块与 Pycon 块:引入 blacken-docs
  4. 选型验证:由于官方提示工具间存在不一致,落地前应在项目文档语料上分别运行两个工具,对比其输出差异(尤其是 Pycon 块、LaTeX minted 环境、docstring 内嵌代码块等边界形态),以当前仓库版本的实际输出为准做决策;
  5. 跟踪上游:该集成文档本身是 Black 版本 26.3.x 期间新增的(见 CHANGES.md "Integrations" 小节中 "Added documentation for doctest formatting tools" 条目,PR #4916),后续版本可能更新工具清单,建议定期回看 docs/integrations/doctest_formatting.md

小结

Black 的原则是"格式化代码,不揣测文档":visit_STRINGfix_multiline_docstring 只对 docstring 的字符串风格与缩进做确定性处理,从不解析其中的 doctest。而 doctest 与文档代码块的格式化由 blackdoc(Python 文件内的 doctest)和 blacken-docs(文档文件中的 Python / Pycon 代码块)按各自的识别规则补齐。理解这一分层边界,就能在项目中把"Black 管代码、专用工具管文档内代码"的完整格式化流水线搭起来,同时保留对工具间行为差异的验证环节。

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

项目优选

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