ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

pytest 自定义目录收集器:基于 manifest.json 定制目录收集策略

2026/9/15 18:27:10 拓冰建站 浏览量
pytest 自定义目录收集器:基于 manifest.json 定制目录收集策略 pytest 自定义目录收集器基于 manifest.json 定制目录收集策略【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest导读pytest 的收集collection阶段负责把命令行传入的路径展开成一个个测试节点目录、模块、函数。默认情况下pytest 对有__init__.py的目录使用pytest.Package收集对其他目录使用pytest.Dir收集。本文基于 doc/en/example/customdirectory.rst 提供的官方示例讲解如何编写自定义的pytest.Directory收集器并通过pytest_collect_directory钩子按目录接管收集逻辑——例如让目录通过一份manifest.json白名单精确控制哪些测试文件参与收集。读完本文你将掌握自定义目录收集器的完整实现套路、钩子优先级语义以及用--collect-only验证收集树的方法。一、默认目录收集策略Dir与Package在自定义之前先理解 pytest 默认怎么收集一个目录。官方文档给出的结论是目录中存在__init__.py文件时pytest 用pytest.Package收集该目录其他目录一律用pytest.Dir收集。这一默认行为在源码中有明确对应实现。pytest_collect_directory钩子的默认实现之一位于 src/_pytest/python.pydef pytest_collect_directory( path: Path, parent: nodes.Collector ) - nodes.Collector | None: pkginit path / __init__.py try: has_pkginit pkginit.is_file() except PermissionError: return None if has_pkginit: return Package.from_parent(parent, pathpath) return None而 src/_pytest/main.py 中的另一个核心实现则无条件返回Dirdef pytest_collect_directory( path: Path, parent: nodes.Collector ) - nodes.Collector | None: return Dir.from_parent(parent, pathpath)Dir与Package都是pytest.Directory收集器的子类Dir的定义见 src/_pytest/main.py并标注versionadded:: 8.0二者的职责都是把目录下的条目进一步收集为子节点。由此可见按目录自定义收集方式正是pytest_collect_directory这个钩子存在的意义默认实现只区分是不是 Python 包而你可以让它区分目录里有没有manifest.json。二、核心机制pytest_collect_directory钩子要接管目录收集需要编写自己的pytest.Directory子类并通过pytest_collect_directory钩子把它挂到收集链上。该钩子的完整契约定义在 src/_pytest/hookspec.pyhookspec(firstresultTrue) def pytest_collect_directory(path: Path, parent: Collector) - Collector | None: Create a :class:~pytest.Collector for the given directory, or None if not relevant. 几个关键语义直接决定插件的写法firstresultTrue这是一个首个结果即采用钩子。插件链上的实现按序被调用第一个返回非None结果的实现胜出后续实现不再执行。因此你的钩子实现命中就返回自定义收集器未命中就返回None让调用链自然回落到默认的Dir/Package实现。返回值为获得最佳效果返回的收集器应该是pytest.Directory的子类官方并不强制但实践上如此并且新节点必须以调用方传入的parent作为父节点——这正是示例中ManifestDirectory.from_parent(parentparent, pathpath)的由来。作用范围pytest_collect_directory属于可以在任意 conftest 中实现的钩子。对于某个待收集路径只有其父目录链上的 conftest 会被咨询且若路径本身是目录该目录自己的 conftest 不会被咨询一个目录不能忽略它自己。这意味着你可以在某个子目录的父级 conftest 里局部启用自定义收集而不影响整个项目。版本该钩子自 pytest 8.0 起引入versionadded:: 8.0当前仓库的 hookspec 文档直接引用了本文讲解的示例See :ref:custom directory collectors。三、完整示例目录 manifest 文件插件官方示例的目标是允许目录放置一份manifest.json用它声明该目录应当收集哪些文件从而在按目录的粒度上定制收集。3.1 conftest.py自定义收集器插件以下是完整的插件实现位于 doc/en/example/customdirectory/conftest.py# content of conftest.py from __future__ import annotations import json import pytest class ManifestDirectory(pytest.Directory): def collect(self): # The standard pytest behavior is to loop over all test_*.py files and # call pytest_collect_file on each file. This collector instead reads # the manifest.json file and only calls pytest_collect_file for the # files defined there. manifest_path self.path / manifest.json manifest json.loads(manifest_path.read_text(encodingutf-8)) ihook self.ihook for file in manifest[files]: yield from ihook.pytest_collect_file( file_pathself.path / file, parentself ) pytest.hookimpl def pytest_collect_directory(path, parent): # Use our custom collector for directories containing a manifest.json file. if path.joinpath(manifest.json).is_file(): return ManifestDirectory.from_parent(parentparent, pathpath) # Otherwise fallback to the standard behavior. return None拆解这个插件的两个组成部分ManifestDirectory(pytest.Directory)是一个自定义收集器核心逻辑在collect()方法中读取目录下的manifest.json注意使用self.path即收集器对应的目录路径仅对manifest[files]列表中的每个文件调用ihook.pytest_collect_file(...)注释中特别点明了与默认行为的差异标准做法是遍历目录下所有test_*.py文件并对每个文件触发pytest_collect_file而这个收集器只认清单不认命名规则。pytest_collect_directory钩子实现是路由逻辑目录存在manifest.json→ 返回ManifestDirectory实例命中即止firstresult语义否则返回None把决策权交还给默认实现Dir/Package。3.2 manifest.json收集清单在tests/目录下放置清单文件 doc/en/example/customdirectory/tests/manifest.json{ files: [ test_first.py, test_second.py ] }当前示例只支持files这一种键一个简单的文件列表。正如文档所说你可以在此基础上继续扩展其他键例如排除规则exclusions与通配模式globs让清单从白名单进化成完整的收集 DSL。3.3 测试文件与配置文件三个候选测试文件doc/en/example/customdirectory/tests/test_first.py# content of test_first.py from __future__ import annotations def test_1(): passdoc/en/example/customdirectory/tests/test_second.py# content of test_second.py from __future__ import annotations def test_2(): passdoc/en/example/customdirectory/tests/test_third.py# content of test_third.py from __future__ import annotations def test_3(): pass注意目录中还有一个空的 doc/en/example/customdirectory/pytest.ini用于将该目录标记为独立的 pytest 配置根示例运行输出中configfile: pytest.ini正来源于此。四、运行验证收集结果与收集树4.1 执行测试在customdirectory目录下直接运行 pytestcustomdirectory $ pytest test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project/customdirectory configfile: pytest.ini collected 2 items tests/test_first.py . [ 50%] tests/test_second.py . [100%] 2 passed in 0.12s 尽管目录里躺着三个测试文件收集到的只有test_first.py与test_second.py两个模块。test_third.py没有被执行因为它没有被列入 manifest——这正是自定义目录收集器带来的行为差异收集入口由清单决定而非由test_*.py命名规则决定。4.2 查看收集树用--collect-only可以直观确认自定义收集器确实出现在收集树中customdirectory $ pytest --collect-only test session starts platform linux -- Python 3.x.y, pytest-9.x.y, pluggy-1.x.y rootdir: /home/sweet/project/customdirectory configfile: pytest.ini collected 2 items Dir customdirectory ManifestDirectory tests Module test_first.py Function test_1 Module test_second.py Function test_2 2 tests collected in 0.12s 收集树中清晰呈现了自定义节点的层级ManifestDirectory tests取代了默认的Package tests/Dir tests其下只有清单列出的两个模块。这正是验证自定义收集器是否生效最直接的手段。五、源码级纵深钩子如何被调用自定义收集器之所以能无缝融入 pytest是因为Dir.collect()在递归展开目录条目时本就以钩子的方式委托目录该由谁收集这一决策。看 src/_pytest/main.py 中Dir.collect()的关键循环def collect(self) - Iterable[nodes.Item | nodes.Collector]: config self.config col: nodes.Collector | None ihook self.ihook for direntry in scandir(self.path): if direntry.is_dir(): path Path(direntry.path) if not self.session.isinitpath(path, with_parentsTrue): if ihook.pytest_ignore_collect(collection_pathpath, configconfig): continue col ihook.pytest_collect_directory(pathpath, parentself) if col is not None: yield col elif direntry.is_file(): path Path(direntry.path) if not self.session.isinitpath(path): if ihook.pytest_ignore_collect(collection_pathpath, configconfig): continue cols ihook.pytest_collect_file(file_pathpath, parentself) yield from cols可以看到两条独立的委托路径对子目录先经pytest_ignore_collect过滤再调用pytest_collect_directory返回非None的收集器即作为子节点产出对文件调用pytest_collect_file收集模块/测试项。此外Session在收集命令行初始路径时同样会调用pytest_collect_directory见 src/_pytest/main.py也就是说自定义收集器不仅作用于目录内部还能直接接管以该目录作为命令行参数的顶层收集入口。示例的ManifestDirectory.collect()复用了pytest_collect_file钩子默认实现在 src/_pytest/python.py负责匹配.py后缀与python_files配置因此自定义收集器产出的模块节点与默认收集完全同构报告、断言、fixture 等后续阶段不需要任何额外适配。六、扩展思路从白名单到完整收集规则官方示例刻意保持最小——manifest.json只支持一个files键。但从插件结构可以自然推演出更多扩展方向排除规则在清单中增加exclude: [test_legacy.py]收集时对files过滤后再调用pytest_collect_file通配模式支持globs: [test_*.py]配合pathlib.Path.glob展开兼顾白名单与默认命名规则多层清单ManifestDirectory自身也是Directory其collect()同样会触发对子目录的pytest_collect_directory递归天然支持嵌套目录的逐层定制条件化收集根据self.config中的命令行选项如--fast动态决定读取哪份清单。这套模式的价值在于默认的目录 → 包/普通目录 → 文件 → 函数收集链是分层的pytest_collect_directory恰好提供了在任意一层目录插入自定义收集器的插槽且firstresult语义让局部自定义 全局兜底的叠加成为可能。七、仓库中的配套验证本示例并非孤立文档同名实现也存在于测试套件中位于 testing/example_scripts/customdirectory/conftest.py包含ManifestDirectory类与检查manifest.json的pytest_collect_directory钩子实现对应的tests/目录同样带有 testing/example_scripts/customdirectory/tests/manifest.json。这说明该示例会作为真实场景被 pytest 自身的回归测试反复验证文档中的运行输出collected 2 items、ManifestDirectory tests收集树是有实测依据的。小结自定义目录收集器的完整套路可以概括为四步继承pytest.Directory重写collect()用你自己的规则决定子节点从何而来实现pytest_collect_directory钩子按目录特征如是否存在manifest.json返回自定义收集器或None用from_parent构造节点确保parent来自钩子参数用pytest --collect-only验证收集树中出现你的自定义节点。把握住pytest_collect_directory的firstresult语义与返回None即回退默认的设计你就能在保持 pytest 原生收集能力的同时为任意目录注入自定义的收集规则。【免费下载链接】pytestThe pytest framework makes it easy to write small tests, yet scales to support complex functional testing项目地址: https://gitcode.com/GitHub_Trending/py/pytest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考