Black 文件收集与发现机制详解:include/exclude 规则、.gitignore 联动与格式化缓存
【免费下载链接】blackThe uncompromising Python code formatter项目地址: https://gitcode.com/GitHub_Trending/bl/black
本篇围绕 Black 的"文件收集与发现"(File collection and discovery)展开:它如何从你传入的文件或目录中筛选出真正需要格式化的 Python 文件,如何用--include/--exclude等正则规则控制匹配范围,如何借助按用户隔离的磁盘缓存跳过未修改的文件,以及如何自动联动.gitignore。读完后,你可以完整掌控 Black 在递归扫描、排除规则组合、缓存命中与 CI 场景下的全部行为细节。
文件收集与发现的总体流程
你可以把文件直接传给 Black,也可以传入目录,让 Black 自动遍历(walk)这些目录并收集待格式化的文件。它自动决定哪些文件要格式化、哪些要跳过,依据有三类:
- include 正则:路径必须匹配
--include指定的模式才会被收集; - exclude 系列正则:
--exclude、--extend-exclude、--force-exclude依次做排除; - 文件修改时间(mtime)等信息:结合缓存判断文件自上次格式化后是否变化。
从源码结构看,整个流程由两条主线完成:
- get_sources()(src/black/init.py)负责"计算要格式化哪些文件"。它遍历命令行传入的
src参数:单文件直接加入候选集(但仍会检查--force-exclude);目录则调用gen_python_files()递归收集。 - gen_python_files()(src/black/files.py)是目录遍历的核心生成器,按固定顺序对每个子路径做判断:先查
.gitignore规则,再依次查--exclude、--extend-exclude、--force-exclude正则,随后跳过指向项目根目录之外的符号链接,最后仅当路径匹配 include 正则(且为.ipynb时已安装 Jupyter 依赖)时才yield该文件。
值得注意的是,gen_python_files()的 docstring 明确指出:它生成的是"未被exclude_regex、extend_exclude、force_exclude正则排除,但被include正则包含"的文件,并且指向root目录之外的符号链接一律忽略(resolves_outside_root_or_cannot_stat()会负责检测,见 src/black/files.py)。
另外,--verbose模式下 Black 会把每个被排除的路径及原因打印出来(report.path_ignored),这也是排查"为什么这个文件没被格式化"的第一手依据。
include 与 exclude 正则规则
以下规则全部定义在 main() 的 click 选项中:
| 选项 | 作用 | 默认值 |
|---|---|---|
--include | 匹配"应被包含"的文件与目录;空值表示不过滤文件名,全部包含 | (\.pyi?|\.ipynb)$ |
--exclude | 匹配"应被排除"的文件与目录;会覆盖所有默认排除规则,且会使.gitignore自动忽略失效 | 见下方DEFAULT_EXCLUDES |
--extend-exclude | 与--exclude类似,但在默认排除规则之上追加排除项,而不是覆盖 | 无(仅在默认排除之上追加) |
--force-exclude | 即使文件被显式传作参数也会排除;适合 pre-commit 钩子、编辑器插件按"变更文件列表"调用 Black 的场景 | 无 |
两个内置默认正则定义在 src/black/const.py:
DEFAULT_INCLUDES = r"(\.pyi?|\.ipynb)$" DEFAULT_EXCLUDES = ( r"/(\.direnv|\.eggs|\.git|\.hg|\.ipynb_checkpoints|\.mypy_cache|\.nox|" r"\.pytest_cache|\.ruff_cache|\.tox|\.svn|\.venv|\.vscode|" r"__pypackages__|_build|buck-out|build|dist|venv)/" )使用要点(与官方帮助文本一致):
- 所有平台的目录分隔符都用正斜杠(包括 Windows);
--exclude一旦设置,DEFAULT_EXCLUDES即被整体替换("An empty value means no paths are excluded");- 匹配在
get_sources()中统一以/<root 相对路径>的形式进行,目录会补一个尾部/(见 src/black/files.py),所以正则写/build/这类带斜杠的锚定写法才能精确命中目录。
跳过未修改文件:按用户隔离的格式化缓存
Black 会记住自己已经格式化过的文件,除非使用了--diff标志,或者代码经由标准输入传入。这部分信息按用户(per-user)存储,存放位置取决于 Black 版本与操作系统,且该缓存文件不具可移植性。各系统上的标准位置为:
- Windows:
C:\Users\<username>\AppData\Local\black\black\Cache\<version>\cache.<line-length>.<file-mode>.pickle - macOS:
/Users/<username>/Library/Caches/black/<version>/cache.<line-length>.<file-mode>.pickle - Linux:
/home/<username>/.cache/black/<version>/cache.<line-length>.<file-mode>.pickle
其中file-mode是一个整型标志位,用于区分该文件当时是按 3.6+ 语法格式化、按.pyi格式化,还是省略了字符串归一化。
缓存目录如何确定(源码印证)
以上位置在源码中的产生逻辑见 get_cache_dir()(src/black/cache.py):
def get_cache_dir() -> Path: default_cache_dir = user_cache_dir("black") cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir)) cache_dir = cache_dir / __version__ return cache_dir即:默认取platformdirs给出的用户缓存目录下的black/子目录(对应上表三大系统的标准路径),再拼接当前版本号__version__——这解释了为什么缓存路径中带有<version>段,升级版本后会自动使用新目录。缓存文件本身由 get_cache_file() 生成,命名规则为cache.<mode.get_cache_key()>.pickle,其中mode键由 Mode.get_cache_key() 生成,对应文档中说的cache.<line-length>.<file-mode>.pickle。
覆盖缓存位置:BLACK_CACHE_DIR 与 XDG_CACHE_HOME
要在所有系统上覆盖上述文件的位置,设置环境变量BLACK_CACHE_DIR为你期望的目录即可;在 macOS 与 Linux 上也可以设置XDG_CACHE_HOME。例如,想让缓存写入当前运行 Black 的目录:
BLACK_CACHE_DIR=.cache/blackBlack 随后会把上述缓存文件写进.cache/black。当两个变量同时设置时,BLACK_CACHE_DIR优先——这与源码中os.environ.get("BLACK_CACHE_DIR", default_cache_dir)的取值顺序完全一致。
缓存如何判定"文件已变化"
Cache 类为每个文件保存一条FileData(st_mtime, st_size, hash)记录,其中 hash 是文件内容的 SHA-256(hash_digest())。is_changed() 的判定策略是三级递进、逐级变重的:
- 缓存中没有该文件的记录 → 视为已变化;
st_size与记录不一致 → 已变化;st_mtime不一致时,才计算内容 SHA-256 并与记录比对(内容相同则视为未变化)。
也就是说,"修改时间"并不是唯一依据——即使 mtime 变了,只要内容哈希一致,Black 仍会跳过该文件。写入方面,Cache.write() 先用tempfile.NamedTemporaryFile落盘再os.replace()原子替换,避免缓存文件损坏;读取时 Cache.read() 对 pickle 解析异常(UnpicklingError、EOFError、权限错误等)全部做了兜底,直接返回空缓存并等待下次写入修复。
使用 --no-cache 禁用缓存
如果你需要 Black 每次都做全新分析,既不读取也不更新磁盘缓存,使用--no-cache标志即可。提供了该标志后,Black 对 per-user 缓存"既不读也不写"。适用于:调试、希望在 CI 中获得确定性全新运行的场景,或者你怀疑缓存已损坏时。
python -m black --no-cache .该选项定义见 main() 的 click 选项,帮助文本为 "Skip reading and writing the cache, forcing Black to reformat all included files."。在执行链路上,reformat_one() 与 reformat_many() 中的处理是:
cache = None if no_cache else Cache.read(mode)即no_cache=True时直接不构造Cache对象,后续所有基于filtered_cached()的跳过逻辑随之失效,每个文件都会走完整格式化流程。
.gitignore 联动:默认生效、--extend-exclude 共存
如果未设置--exclude,且仓库中存在.gitignore,Black 会自动忽略其中列出的文件与目录。如果希望 Black 继续遵守.gitignore,同时又要自定义排除规则,请使用--extend-exclude而不是--exclude。
这一"是否启用 gitignore"的开关在源码中体现得非常直白。get_sources() 中:
using_default_exclude = exclude is None ... if using_default_exclude: gitignore = { root: root_gitignore, path: get_gitignore(path), }只有当用户没有显式传--exclude时,gitignore参数才会被填充为"项目根 + 待扫描目录"两个级别的.gitignore规则集;一旦传了--exclude,gitignore保持为None,gen_python_files()中的 gitignore 检查(src/black/files.py)便整体关闭。--extend-exclude则不修改exclude本身,因此 gitignore 联动得以保留——这正是文档推荐它的原因。
几个源码层面值得留意的细节:
- 规则解析:get_gitignore() 用
pathspec库的GitIgnoreSpec.from_lines()解析.gitignore;解析失败会打印错误并抛出GitIgnorePatternError。 - 嵌套目录逐级匹配:
gen_python_files()递归进入子目录时,会把"当前目录的.gitignore"合并进gitignore_dict再向下传递;_path_is_ignored() 按"从最不具体到最具体"的顺序逐条规则匹配,路径按相对各.gitignore所在目录的方式计算,目录名还会补一个尾部/以便精确匹配目录规则。 - 符号链接保护:指向项目根之外的符号链接会被跳过并计入 ignored 报告(resolves_outside_root_or_cannot_stat()),防止 Black 沿链接格式化到仓库之外的代码。
- 测试覆盖:仓库中提供了多组 gitignore 场景的测试数据,可用来对照行为,例如 tests/data/nested_gitignore_tests/(嵌套
.gitignore)、tests/data/ignore_directory_gitignore_tests/(整目录忽略)、tests/data/ignore_subfolders_gitignore_tests/(子文件夹忽略)以及 tests/data/invalid_gitignore_tests/(非法规则处理)。
小结与延伸阅读
回到开头的那句话:Black 用"include 正则 + exclude 系列正则 + 修改时间/内容缓存"三套机制自动决定格式化范围。核心要点回顾:
- 默认只处理
.py、.pyi、.ipynb(DEFAULT_INCLUDES),并默认跳过.git、build、.venv等常见生成物目录(DEFAULT_EXCLUDES); --exclude会同时关闭默认排除与.gitignore联动;想"追加排除"就用--extend-exclude,想"排除显式传入的文件"就用--force-exclude;- 缓存按用户存放、按版本与
mode键分文件,BLACK_CACHE_DIR优先级高于XDG_CACHE_HOME,--no-cache可完全绕过读写。
可继续深入阅读的仓库文件:
- src/black/files.py:
get_gitignore()、gen_python_files()、find_project_root()(项目根识别基于.git/.hg/含[tool.black]的pyproject.toml); - src/black/cache.py:
Cache类的读取、变化判定与原子写入实现; - src/black/init.py:
--include/--exclude/--extend-exclude/--force-exclude/--no-cache的选项定义与get_sources()收集流程。
【免费下载链接】blackThe uncompromising Python code formatter项目地址: https://gitcode.com/GitHub_Trending/bl/black
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考