将 Black 引入既有 Python 项目:格式化迁移与 git blame 无损落地实战
将 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-commit。docs/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 场景下不会生效。文档给出两种推荐做法:
- 首选:直接使用 pre-commit 的
exclude字段过滤文件,让文件根本不会被传给 Black:
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 26.5.1
hooks:
- id: black
exclude: ^migrations/|^generated/
- 备选:使用 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 = 88、target-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,默认排除.git、venv、build、dist等常见目录)。未显式设置--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.md 与 docs/usage_and_configuration/the_basics.md 分别给出了版本控制与退出码语义的权威说明,可作为你团队接入时的核对清单:
- 迁移当日:
black .全库格式化 → 单一提交 → 哈希写入.git-blame-ignore-revs→git config blame.ignoreRevsFile .git-blame-ignore-revs; - 之后每次提交:pre-commit 钩子自动格式化与检查;
- CI:
black . --check返回非零即失败,防止任何非 Black 风格代码合入主干; - 若日后升级 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 的稳定风格。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00