首页
/ 将 Black 引入既有 Python 项目:格式化迁移与 git blame 无损落地实战

将 Black 引入既有 Python 项目:格式化迁移与 git blame 无损落地实战

2026-09-08 11:18:07作者:何将鹤

将 Black(The uncompromising Python code formatter)引入存量 Python 代码库,最常被团队拿来反对的理由是"一次全量格式化会毁掉 git blame"。本指南基于仓库内 docs/guides/introducing_black_to_your_project.md 的官方迁移指引,先讲透 Git 原生 --ignore-revs-file 机制的用法,再结合仓库中的 pre-commit 集成、pyproject.toml 配置与命令行选项,给出从"一次性大规模格式化提交"到"CI 持续强制保持"的完整迁移实战方案。读完你将掌握一套既能把代码格式统一交给 Black、又不丢失历史追溯信息的可落地流程。

说明:仓库内该指南原文标注为 "incomplete",目前主要覆盖了与 git blame 相关的迁移部分;本文以它为核心骨架,并补充仓库中其他官方文档与源码可验证的配套操作,供你在迁移时继续展开。

迁移前:Black 是什么、需要准备什么

Black 是一个"风格固执己见"的代码格式化器:它不提供大量风格选项,而是给出稳定、可复现的输出。仓库 README.md 的安装说明给出了最简用法:

pip install black          # 需要 Python 3.10+(见根目录 pyproject.toml 的 requires-python)
python -m black {源文件或目录}   # 直接以脚本运行不可用时,可用模块方式

如需格式化 Jupyter Notebook,则安装 pip install "black[jupyter]"。仓库自身在 pyproject.toml 中把 Black 用于自我格式化(连根配置都开着 unstable = true),并在 CHANGES.md 中持续记录格式行为变化,说明它已被长期用于真实项目。

对存量项目而言,迁移意味着:整个代码库的缩进、引号、空行、续行方式将统一成 Black 风格。由此产生的"全库一次性大提交"正是接下来要处理的核心矛盾。

回应经典顾虑:git blame 会不会被一次格式化提交毁掉?

"用了 Black 之后 git blame 的每一行都会指到格式化提交上,历史贡献信息全没了"——这是社区中一个长期存在的反对理由。它曾经是有效的担忧,但自 Git 2.23 起,Git 原生支持在 blame 时忽略特定修订版本:

  • 对单个修订,使用 git blame --ignore-rev <40位commit哈希>
  • 对一组修订,使用 git blame --ignore-revs-file <文件>,把需要忽略的修订列表写进文件。

被忽略的修订在 blame 归属计算中会被跳过:由被忽略修订改动的行,会被归因到上一个修改过这些行的修订。也就是说,格式化提交造成的"整文件刷屏式 blame"会被隐藏,你仍能看到每一行真正有意义的业务改动来自谁。

仓库指南原文(docs/guides/introducing_black_to_your_project.md)建议的流程是:迁移时把所有代码一次性格式化并提交——最好是一个单一的大规模提交——然后把这个提交的完整 40 位 commit 标识符写入项目根目录下一个通常命名为 .git-blame-ignore-revs 的文件。

# Migrate code style to Black
5b4ab991dede475d393e9d69ec388fd6bd949699

文件格式约定很简单:每行一条完整的 40 位 commit 哈希,井号开头是注释,方便后续追加说明,例如历史上可能有多轮预览风格切换、旧版 Black 迁移提交,都可逐行追加,让文件成为一份可审计的"格式化提交登记表"。

之后执行 blame 时显式传入该文件:

$ git blame important.py --ignore-revs-file .git-blame-ignore-revs
7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 1) def very_important_function(text, file):
abdfd8b0 (Alice Doe  2019-09-23 11:39:32 -0400 2)     text = text.lstrip()
7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 3)     with open(file, "r+") as f:
7a1ae265 (John Smith 2019-04-15 15:55:13 -0400 4)         f.write(formatted)

可以注意到,第 2 行虽由"格式化迁移提交"重写过,但 blame 正确回溯到了真正的原作者 abdfd8b0

你还可以在仓库层面配置 Git,让每次 git blame 都自动读取该忽略文件,无需手写参数:

$ git config blame.ignoreRevsFile .git-blame-ignore-revs

把这一行放进仓库级的 .git/config(或在团队 onboarding 文档里让每个成员执行一次),就能让"打开 blame 看历史"成为默认行为。

唯一需要注意的 caveat 是:部分在线 Git 托管平台的网页端 blame UI 尚不支持忽略修订,因此在那些平台上查看 blame 仍会被格式化提交刷屏。目前 GitHub 的 blame 视图以及 GitLab(自 17.10 版本起)都已默认支持 .git-blame-ignore-revs;其他平台是否支持需自行确认。

落地第一步:把全库格式化提交做"干净"

要让 .git-blame-ignore-revs 真正有效,迁移提交本身要满足两个条件:一次性覆盖全库可被唯一识别。推荐顺序如下。

先用 --diff--check 预览迁移影响面,不直接改写文件:

$ black src/ tests/ --check
would reformat src/foo.py
Oh no! 💥 💔 💥
1 file would be reformatted.
  • --check:不写回文件,仅返回状态码——代码无需改动返回 0;有文件会被重新格式化返回 1;发生内部错误返回 123。它天然适合 CI 与"先看影响范围"的场景。
  • --diff:不写回文件,仅把将产生的 diff 输出到标准输出(可与 --color 配合看彩色 diff),方便你在提交前 review Black 会动哪些行。

这两个旗标在 docs/usage_and_configuration/the_basics.md 中都有完整的行为与退出码说明,可并同时使用。

确认影响面后,执行真正的全量格式化:

black .          # 从当前目录递归收集并就地格式化

然后把全部改动作为单一提交提交,记录下该提交的完整哈希,写入根目录 .git-blame-ignore-revs,并配置 blame.ignoreRevsFile。做完这一步,之后无论新增功能还是修 bug,git blame 看到的都是格式化之前的真实历史归属。

落地第二步:用 pre-commit 让"格式化结果"不倒退

一次性迁移只解决当下;要防止后续提交悄悄带出非 Black 风格的代码,仓库官方推荐的是集成 pre-commitdocs/integrations/source_version_control.md 给出了可直接放入仓库根目录 .pre-commit-config.yaml 的配置:

repos:
  # 使用该镜像仓库可用到 mypyc 编译版 Black,速度约提升 2 倍
  - repo: https://github.com/psf/black-pre-commit-mirror
    rev: 26.5.1
    hooks:
      - id: black
        # 建议填你项目支持的最新 Python 版本,
        # 或改用 pre-commit 的 default_language_version
        language_version: python3.11

要点与注意事项:

  • rev 应固定为某个具体发布版本,不要使用分支等可变引用——pre-commit 钩子不会像你预期的那样自动跟随分支更新,固定版本才能保证不同开发者、CI 与本地结果一致。
  • 若要额外把 Jupyter Notebook 纳入格式化范围,把钩子 id: black 换成 id: black-jupyter(该钩子自 21.8b0 起可用),更多细节见 docs/guides/using_black_with_jupyter_notebooks.md
  • 由于迁移时经常有些目录(如 migrations/generated/)本就不需要被 Black 触碰,记得预先在钩子配置里把它们排除掉(详见下文"排除文件")。

排除文件的正确姿势:pre-commit 与 Black 的排除机制不同

一个容易踩的坑:pre-commit 是把文件直接通过命令行传给 Black,而不是让 Black 做递归目录发现。因此 Black 的 --exclude(仅作用于递归遍历阶段)在 pre-commit 场景下不会生效。文档给出两种推荐做法:

  1. 首选:直接使用 pre-commit 的 exclude 字段过滤文件,让文件根本不会被传给 Black:
repos:
  - repo: https://github.com/psf/black-pre-commit-mirror
    rev: 26.5.1
    hooks:
      - id: black
        exclude: ^migrations/|^generated/
  1. 备选:使用 Black 的 force-exclude 配置(自 20.8b0 起支持,专为"文件被显式以命令行参数传入"的场景设计),即使文件被显式传入也会被排除:
[tool.black]
force-exclude = '''
(
  ^migrations/
  | ^generated/
)
'''

两种方式都写进了 docs/integrations/source_version_control.md,可结合仓库实际选择。建议用 pre-commit 的 exclude 在源头过滤,减少无谓的进程启动开销。

沉淀配置:用 pyproject.toml 固定迁移时的各项参数

Black 支持从项目根目录的 pyproject.toml[tool.black] 段读取配置,键名即 CLI 长选项去掉前导 --(如 line-length)。这很适合把迁移时定下的参数变成全团队共享的项目规范。

配置查找逻辑(docs/usage_and_configuration/the_basics.md):Black 从命令行传入文件的公共基目录开始找含 [tool.black]pyproject.toml,找不到则向上层目录寻找,直到命中、或遇到 .git/.hg 目录、或到达文件系统根目录;也可用 --config 显式指定配置文件(此时不再查找其他文件);运行 --verbose 可看到实际使用的配置文件路径。

一个覆盖主要迁移场景的最小示例:

[tool.black]
line-length = 88          # 每行允许的最大字符数,默认 88,可覆盖
target-version = ['py37'] # 目标 Python 版本,影响语法解析与风格决策
include = '\.pyi?$'       # 递归时纳入的文件模式
# 'extend-exclude' 在默认排除之外追加排除规则
extend-exclude = '''
# 以 ^/ 开头的正则只作用于项目根目录下的文件/目录
(
  ^/foo.py    # 排除项目根目录下名为 foo.py 的文件
  | .*_pb2.py  # 排除全项目内自动生成的 Protocol Buffer 文件
)
'''

需要留意的 TOML 细节:正则表达式必须用单引号字符串(等价于 Python 的 r-string);多行字符串会被当作 verbose 正则处理,此时如需匹配真正的空格,用 [ ] 表示一个显著空格。仓库根目录的 pyproject.toml 正是这样自我格式化的实例:它设置了 line-length = 88target-version = ["py310"]、用 extend-exclude 排除 tests/data/profiling/,并打开了 unstable 风格用于自身开发。

若希望所有协作者使用完全一致的格式输出,还可在配置中固定版本号,防止不同 Black 版本输出微差:

[tool.black]
line-length = 88
target-version = ["py311"]
required-version = "26"   # 也接受主版本号或完整版本号,如 "26.5.1"
skip-string-normalization = false
skip-magic-trailing-comma = false
preview = false

required-version 对应的 CLI 行为见 docs/usage_and_configuration/the_basics.md:运行版本不匹配时报错退出,可配合 Black 的稳定性策略(见 docs/the_black_code_style/index.md),保证"稳定风格 + 允许非格式相关改进"。需要强调的是,Black 本身强调"开箱即用"的默认值就能让你的代码与成千上万个被 Black 格式化的项目风格一致,如果你不确定要不要配置,答案通常是不需要额外配置;配置文件主要服务于排除目录、目标版本与团队版本一致性等真实需求。

迁移的工程细节与注意事项

文件收集、.gitignore 与缓存

  • Black 可直接传入文件,也可传入目录递归收集;收集时受 --include/--exclude/--extend-exclude 正则影响(默认纳入 .pyi.ipynb,默认排除 .gitvenvbuilddist 等常见目录)。未显式设置 --exclude 时,Black 还会自动忽略 .gitignore 中列出的文件——这意味着被 git 忽略的生成目录通常不会被误格式化;需要自定义排除规则同时又想保留 .gitignore 行为时,请用 --extend-exclude 而不是覆盖默认值的 --exclude。完整机制见 docs/usage_and_configuration/file_collection_and_discovery.md
  • Black 会把"已格式化且未改动"的文件记入按用户隔离的缓存,二次运行会跳过它们以提速。迁移时或 CI 中若希望每次都做全新分析(例如排查缓存问题、确保确定性结果),可加 --no-cache;缓存目录可用环境变量 BLACK_CACHE_DIR 指定。
  • 想只格式化指定行范围(如编辑器"Format Selection"),可用 --line-ranges=1-10 这类参数,但不支持一次处理多文件或 Notebook,也不能写进 pyproject.toml

迁移提交后的日常节奏

迁移完成后的理想日常是:格式化交给工具自动完成,提交前 pre-commit 钩子兜底,CI 用 --check 卡口。仓库的 docs/integrations/source_version_control.mddocs/usage_and_configuration/the_basics.md 分别给出了版本控制与退出码语义的权威说明,可作为你团队接入时的核对清单:

  1. 迁移当日:black . 全库格式化 → 单一提交 → 哈希写入 .git-blame-ignore-revsgit config blame.ignoreRevsFile .git-blame-ignore-revs
  2. 之后每次提交:pre-commit 钩子自动格式化与检查;
  3. CI:black . --check 返回非零即失败,防止任何非 Black 风格代码合入主干;
  4. 若日后升级 Black 或切换 --preview/--unstable 风格并再次全量重排,把新提交哈希同样追加进 .git-blame-ignore-revs,blame 依旧干净。

写在最后

"用了 Black 就毁了 blame"在 Git 2.23 之后已不再成立:--ignore-revs-file 让你能精确声明"哪些提交纯粹是格式化,不应参与 blame 归属计算",配合 .git-blame-ignore-revs 的团队共享与 blame.ignoreRevsFile 的默认化,一次大规模格式化迁移可以在不牺牲历史可追溯性的前提下完成。剩下的唯一遗留成本,是少数尚未支持该机制的在线平台网页端 blame 视图仍会显示格式化提交——这属于平台能力差异,而非代码库本身的损失。以本指南为核心流程,再结合 docs/usage_and_configuration/the_basics.md(CLI/配置速查)、docs/integrations/source_version_control.md(pre-commit 集成)与 docs/usage_and_configuration/file_collection_and_discovery.md(文件发现与排除规则),你就能在保持 git 历史纯净的前提下,把代码库一步步引入 Black 的稳定风格。

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

项目优选

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