news 2026/9/14 8:04:51

marimo 仓库的 pytest-changed 插件:基于 Ruff 依赖图精准运行受影响测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
marimo 仓库的 pytest-changed 插件:基于 Ruff 依赖图精准运行受影响测试

marimo 仓库的 pytest-changed 插件:基于 Ruff 依赖图精准运行受影响测试

【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo

本文介绍 marimo 开源仓库中内置的 pytest 插件packages/pytest_changed:它通过git diff找出你改动过的 Python 文件,再利用 Ruff 的analyze graph能力构建项目依赖图,经 BFS 图遍历定位所有受影响文件,最终只运行其中真正可能受影响的测试。读完本文,你将掌握该插件的完整用法(对比分支、预览、包含未改动测试、限定目录)、全部命令行参数,以及其底层五个阶段的源码级实现原理,可直接在 marimo 或任何基于 git + pytest 的 Python 项目中落地使用。

插件解决的问题:全量测试的浪费

在一个大型 Python 仓库中,每次提交前跑完整测试套件非常耗时。以 marimo 仓库为例,其 pyproject.toml 定义了testtest-optional两组依赖,测试覆盖了运行时(tests/_runtime/)、AST 编译(tests/_ast/)、服务端(tests/_server/)、插件(tests/_plugins/)等几十个目录,全量回归的成本很高。

packages/pytest_changed的定位是按变更智能缩小测试范围:它分析你的 git 变更,通过依赖图搜索找出所有受变更影响的文件,然后只运行可能被影响的测试。它不依赖任何启发式规则(如"改了哪个模块就测哪个模块"),而是基于 Ruff 对 Python 导入关系(包括字符串形式的导入)的静态分析,做到既有精确度又避免遗漏下游依赖方。

快速上手:激活与基本用法

该插件以 pytest 内置插件(-p)方式激活,无需安装额外包。在 marimo 仓库根目录执行:

pytest -p packages.pytest_changed --changed-from=main

--changed-from=main指定与main分支对比,找出自main以来发生变更的文件,并只运行受影响的测试。

使用 uv 运行(推荐,可同时锁定 Python 版本与依赖组):

uv run --python 3.12 --group test pytest -p packages.pytest_changed --changed-from=main tests/

这里--python 3.12指定解释器版本,--group test使用 pyproject.toml 中[dependency-groups]定义的test依赖组(包含pytest~=9.0.2pytest-picked>=0.5.1等),tests/作为测试根目录参数。

对比不同的 git 引用

插件默认支持任意 git ref,常见场景:

# 对比 HEAD:检测暂存区(staged)以来的改动 pytest -p packages.pytest_changed --changed-from=HEAD # 对比远程分支 pytest -p packages.pytest_changed --changed-from=origin/main

从源码看,--changed-from的值会被原样传给git diff --name-only <ref>(见 packages/pytest_changed/init.py),因此只要是 git 能解析的引用(分支名、tag、HEAD~3等)都可以使用。

预览将要运行的测试

不想真正执行测试时,用 pytest 自带的--collect-only仅收集并打印被选中的用例:

pytest -p packages.pytest_changed --changed-from=main --collect-only tests/

这在确认插件选集是否符合预期、或在 CI 调试时非常有用。

包含未改动的测试

--include-unchanged会在受影响的测试之外,额外运行所有其他测试,相当于"受影响测试优先、全量兜底":

pytest -p packages.pytest_changed --changed-from=main --include-unchanged tests/

值得注意的实现细节是:一旦设置了该选项,插件会直接跳过变更检测与依赖图构建(这两步是整个流程中最昂贵的部分),因为最终反正要跑全部测试。对应源码在pytest_configure中:

if config.getoption("include_unchanged"): print_("--include-unchanged set - running all tests") config.option.changed_test_files = None return

见 packages/pytest_changed/init.py。

限定到指定测试目录

插件尊重 pytest 常规的路径过滤语义,你可以在命令尾部传入目录,将选集进一步约束到该目录内(该目录会同时成为测试文件过滤的基准):

pytest -p packages.pytest_changed --changed-from=main tests/_runtime/

底层通过get_test_base从 pytest 调用参数中解析第一个存在的目录作为test_base,只有位于该目录下的受影响文件才会被视为测试文件(见 packages/pytest_changed/init.py)。

配置选项一览

插件在pytest_addoption中注册了两个选项,归属ruff-graph参数组(见 packages/pytest_changed/init.py):

选项类型默认值说明
--changed-from=<ref>storeNone要对比的 git 引用(如HEADmainorigin/main)。该选项是激活插件的必要条件,未提供时插件不执行任何分析,pytest 按常规模式运行
--include-unchangedstoreFalse布尔开关,值为true/1/yes(不区分大小写)时开启。开启后除受影响测试外还运行全部未受影响的测试

两个选项的解析行为也可以从源码确认:--changed-from未设置时pytest_configure直接return(L238-L241);--include-unchanged使用type=lambda x: x.lower() in ("true", "1", "yes")解析字符串布尔值(L44)。

工作原理:五个阶段的源码级剖析

插件的执行逻辑完整实现在 packages/pytest_changed/init.py(约 376 行,无第三方依赖,仅用标准库subprocess/json/collections.deque与 pytest API)。其流程分为五步:

阶段 1:用 git diff 找出变更文件

get_changed_files执行git diff --name-only <ref>,只保留存在且后缀为.py的文件:

result = subprocess.run( ["git", "diff", "--name-only", ref], cwd=repo_root, capture_output=True, text=True, check=True, )

见 packages/pytest_changed/init.py。该函数同时返回一个critical_changed标志——如果变更文件中包含pyproject.toml这类影响所有测试的配置文件,插件会放弃缩小范围,直接运行全部测试(L272-L279)。当前仓库中critical_files只包含pyproject.toml(L62),因为它集中定义了依赖、pytest 配置与打包元数据,改动它意味着任何测试的运行时环境都可能变化。

阶段 2:用 ruff analyze graph 构建依赖图

get_dependency_graph调用uvx运行固定版本的 Ruff:

[ "uvx", "ruff@0.15.18", "analyze", "graph", "--detect-string-imports", "--direction", direction, ".", ]

见 packages/pytest_changed/init.py。几个关键决策:

  • 固定ruff@0.15.18:源码注释明确说明,因为 Ruff 的analyze graph尚属实验性功能,固定版本可避免输出格式或行为变化带来的解析失败(L118-L120);
  • --detect-string-imports:让 Ruff 额外识别字符串形式(动态)导入,避免遗漏这类隐式依赖;
  • --direction dependents:图的方向是"谁依赖了我",这正是寻找下游受影响文件的正确方向;
  • 输出为 JSON,解析后统一转换为绝对路径再参与后续遍历(L135-L145)。

阶段 3:BFS 图遍历找出所有受影响文件

find_affected_files以变更文件为起点做广度优先搜索(BFS),沿"dependents"边扩散,用visited集合处理环与去重:

affected: set[Path] = set(changed_files) visited: set[str] = set() queue: deque[Path] = deque(changed_files) while queue: current = queue.popleft() ... dependents = dependency_graph.get(current_str, []) for dependent in dependents: ...

见 packages/pytest_changed/init.py。由于 Python 模块间依赖可能形成环(A 导入 B、B 也导入 A),visited集合保证了遍历必然终止;结果集合包含变更文件本身及其全部间接下游,例如改动marimo/_runtime/cell.py后,直接导入它的模块、再导入这些模块的模块都会被逐层波及。

阶段 4:过滤出测试文件

find_test_files按三条规则过滤受影响文件:文件存在、文件名以test_开头(marimo 的测试命名约定)、且位于test_base目录之下(packages/pytest_changed/init.py)。test_base默认取 pytest 调用参数中第一个存在的目录,否则回退到仓库根目录(L192-L207),这也是上文中"限定目录"用法能生效的原因。

阶段 5:运行选中的测试

插件通过两个 pytest hook 完成最后的选集注入:

  • pytest_configure:执行上述全部分析。找到受影响测试后,将文件列表写入config.option.changed_test_files,并把config.args直接替换为受影响测试文件路径(L316-L322)。分析过程中的关键决策都会打印到终端(变更文件清单、受影响文件数、选中的测试文件),方便人工核验;
  • pytest_collection_modifyitems:收集完成后按changed_test_files对用例做二次筛选。三种分支语义清晰(L338-L376):
    • changed_test_files is None:全量运行(对应--include-unchanged或配置文件变更的场景);
    • 空集合:所有用例被标记为skip,跳过理由为Not affected by changes from <ref>
    • 非空集合:只保留属于受影响文件的用例,其余通过pytest_deselected钩子排除,并打印"运行 N 个用例、跳过 M 个用例"的摘要。

此外,仓库根目录通过git rev-parse --show-toplevel获取(L246-L254),保证在子目录中执行命令也能正确定位仓库。

仓库内的工程化实践:跨版本矩阵脚本

marimo 没有止步于插件的裸调用,而是将其封装进了 scripts/pytest-changed.sh:该脚本在Python 3.10 / 3.11 / 3.12 / 3.13四个版本、test-optional依赖组上循环执行插件测试,并对每个版本汇总通过/失败结果。其核心调用为:

uv run --python "$PY_VERSION" --group "$GROUP" pytest tests/ \ -v \ -k "not test_cli" \ --durations=10 \ -p packages.pytest_changed \ --changed-from="$CHANGED_FROM" \ --include-unchanged=false \ --picked=first \ "$@"

见 scripts/pytest-changed.sh。其中:

  • --python/--group/--from分别控制版本、依赖组与对比引用(默认main);
  • --picked=first来自test依赖组中的pytest-picked插件,与 pytest-changed 配合使用;
  • --include-unchanged=false显式关闭兜底全量,确保矩阵测试只跑受影响用例;
  • 结尾的"$@"允许透传额外 pytest 参数(如-k 'test_foo' -x)。

在 CI 场景下,插件源码还对"对比引用不存在"给出了明确提示:如果 checkout 时未拉取完整历史(例如 GitHub Actions 的actions/checkout默认浅克隆),git diff会失败,此时需要在检出步骤配置fetch-depth: 0(见 packages/pytest_changed/init.py 中的错误信息)。

使用边界与注意事项

结合源码可以总结出以下适用前提与限制,帮助你在自己的项目中正确使用:

  1. 必须位于 git 仓库内:插件依赖git rev-parse --show-toplevel,非 git 目录会直接以错误退出(L253-L254);
  2. 只追踪.py文件git diff结果中非 Python 文件(如前端.ts、配置.json)不会触发测试选择,除非命中pyproject.toml这类关键文件(L85);
  3. 测试文件按test_*.py命名约定识别:如果你的测试命名不符合该约定,需要自行调整find_test_files中的过滤逻辑(L226-L230);
  4. 依赖图依赖 Ruff 的静态分析ruff analyze graph目前仍标记为实验性,插件通过固定ruff@0.15.18版本规避不稳定性;导入关系极为动态(如运行时动态拼接模块名)的场景可能无法被完整捕捉;
  5. --include-unchanged会短路整个分析:它不是"受影响测试优先执行",而是"全部执行",只是用选项语义表达了"不遗漏任何测试"的兜底诉求(L259-L262)。

总结

packages/pytest_changed是 marimo 仓库中一个"小而完整"的工程工具:核心逻辑全部集中在 packages/pytest_changed/init.py 单文件内,通过git diff+ruff analyze graph+ BFS 遍历 + pytest hook 的组合,实现了依赖感知的精准测试选集,并配合 scripts/pytest-changed.sh 在多 Python 版本矩阵中落地。无论你是 marimo 的贡献者,还是想在自有 Python 项目中复刻"改多少、测多少"的开发体验,都可以直接以该插件为模板——其无第三方运行时依赖、仅依赖 git 与 uvx 的设计,让移植成本几乎为零。

【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 8:02:55

DeskcommCRM全解析:从核心模块到私有化部署实践

DeskcommCRM 这个名字我第一次看到的时候&#xff0c;以为是某个团队内部用的客服平台代号&#xff0c;后来真正接触下来才发现&#xff0c;它本质上是一套把“桌面工作台”和“客户沟通”深度绑定的客户关系管理系统。简单说&#xff0c;它不只是一本电子通讯录&#xff0c;而…

作者头像 李华
网站建设 2026/9/14 8:02:29

本地大模型网关CLI实战:从Ollama到LiteLLM的终端统一入口

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 8:01:20

COMSOL多物理场耦合在交流电弧仿真中的应用与优化

1. COMSOL交流电弧模型的核心价值与应用场景交流电弧现象在电力系统、工业加工和科研实验中广泛存在&#xff0c;但传统实验方法难以捕捉其瞬态特性。COMSOL Multiphysics提供的多物理场耦合仿真能力&#xff0c;让我们能够完整复现电弧放电过程中的电磁场、温度场和流体场相互…

作者头像 李华
网站建设 2026/9/14 7:55:28

论文降重工具Paperxie的技术原理与应用实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 7:54:43

混合动力汽车能量管理中的动态规划:原理、MATLAB实现与参数调优

简介&#xff1a;一套基于MATLAB的混合动力汽车能量管理动态规划算法实现&#xff0c;面向新能源汽车控制策略研究人员、车辆工程专业学生以及混动系统仿真工程师&#xff0c;用于解决不同行驶工况下发动机与电动机的功率分配和模式切换优化问题。资源包共4个文件&#xff0c;压…

作者头像 李华