尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ruff 的 Notebook 单元格边界解析:逐 Cell 独立解析、合并 lint 与 mdtest 测试设计

ruff 的 Notebook 单元格边界解析:逐 Cell 独立解析、合并 lint 与 mdtest 测试设计 ruff 的 Notebook 单元格边界解析逐 Cell 独立解析、合并 lint 与 mdtest 测试设计【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruffruff 在 lint Jupyter Notebook.ipynb时面临一个结构性难题单元格的源码片段单独看可能不完整拼接起来又会产生跨单元格的“伪语法”例如末尾的装饰器会“吃掉”下一个单元格的定义。本文围绕仓库中的 mdtest 测试文档 cell-boundaries.md 展开说明 ruff “每个 cell 作为独立模块解析、同时保留一个合并模块用于 lint”的设计如何落地的并完整解析文档中三个测试场景跨 cell 语法错误定位、无 Python 单元格、单元格边界 token的预期行为与验证方式读完后你可以掌握 ruff notebook 解析的边界处理原理以及如何用 mdtest 复现和扩展这些测试。测试载体mdtest 框架如何驱动这份文档cell-boundaries.md 并不是普通说明文档而是一个可执行的测试套件mdtest fixture。仓库通过 mdtest 测试入口 用datatest_stable::harness!以root ../ruff_linter/resources/mdtest、pattern r\.md$扫描该目录下所有.md文件逐个作为测试执行每个.md内部的 fenced code block 会被当作内嵌文件处理。从 ruff_mdtest 的运行实现 可以看到具体流程解析 markdown 套件后在内存文件系统里创建项目根目录/src把内嵌的py、pyi、ipynb、toml代码块按各自文件名写入该目录lang ignore的块会被跳过用文档中[TOML]配置块构造Configuration并解析为 linter 设置对每个写入的文件调用test_contents定义于 ruff_linter/src/test.rs执行 lint得到诊断列表用内联断言# snapshot与snapshot块比对诊断输出不一致即测试失败。这正是为什么文档中的快照路径写作src/syntax-error.ipynb:cell 1:2:6—— 内嵌文件被放在内存根/src下。诊断的渲染、# snapshot断言的匹配以及失败时的 diff 输出由底层 mdtest 库 的validate_inline_snapshot与match_file完成。核心设计每个 cell 独立解析同时保留合并模块文档开篇给出的设计陈述是整篇文章的主线Ruff parses every notebook cell as its own module while retaining a single combined module for linting.ruff 将每个 notebook cell 作为独立模块解析同时保留一个合并模块用于 lint。在源码中可以印证这一设计linter.rs 中的解析分派 显示当source_kind是SourceKind::ipy_notebook时ruff 不会走普通的parse_unchecked而是调用ruff_python_parser::parse_cells_unchecked传入notebook.cell_offsets().content_ranges()各单元格的源码区间作为参数match source_kind.as_ipy_notebook() { Some(notebook) ruff_python_parser::parse_cells_unchecked( source_kind.source_code(), notebook.cell_offsets().content_ranges(), options, ), None ruff_python_parser::parse_unchecked(source_kind.source_code(), options)parse_cells_unchecked的语义正是文档所描述的“逐 cell 模块 合并 lint”每个单元格的词法/语法分析在 cell 边界处收口从而避免一个 cell 中未闭合的结构“越界”解释到下一个 cell而各 cell 的 AST 会合并为一个统一的模块视图使语义类规则未定义名、未使用导入等仍能看到跨 cell 的完整信息。单元格的元数据索引、起止偏移由 ruff_notebook 的 cell 模型 提供。下面三个测试场景分别验证这一设计的三类边界行为。场景一语法错误止步于 cell 边界文档给出的反例非常直观如果把所有 cell 的源码直接拼接第一个 cell 末尾的deco会“合法地”装饰第二个 cell 的def f(): pass语法错误凭空消失。独立解析每个 cell 后这个装饰器在自己的 cell 内找不到被装饰对象错误必须定位到装饰器所在的 cellsyntax-error.ipynb原文档 中的完整内嵌文件{ cells: [ { cell_type: code, execution_count: null, metadata: {}, outputs: [], source: [# snapshot\n, deco] }, { cell_type: code, execution_count: null, metadata: {}, outputs: [], source: [def f(): pass] } ], metadata: {}, nbformat: 4, nbformat_minor: 4 }第一个 cell 中的# snapshot是 mdtest 的内联断言标记对应 mdtest 库中对# snapshot的处理其后的snapshot块给出期望的诊断输出error[invalid-syntax]: Expected class, function definition or async function definition after decorator -- src/syntax-error.ipynb:cell 1:2:6 | 2 | deco | ^三个细节值得注意诊断位置是cell 1:2:6即第一个 cell 内部的第 2 行而不是合并模块的某个偏移——这验证了错误归属到装饰器所在的 cell错误类型是invalid-syntax解析器级语法错误说明该场景在解析阶段就被独立 cell 的解析捕获该 fixture 与 linter.rs 测试模块 中针对resources/test/fixtures/syntax_errors/*.ipynb的 notebook 语法错误测试属于同一类验证目标mdtest 版本则把内嵌 fixture 与断言写进了同一个文档。场景二不含任何 Python 单元格的 Notebook文档中第二个场景只有一条断言式的陈述A notebook containing no Python code cells still parses successfully.不包含任何 Python 代码单元格的 notebook 也能成功解析。对应 fixtureno-code-cells.ipynb{ cells: [ { cell_type: markdown, metadata: {}, source: [# Nothing to check] } ], metadata: {}, nbformat: 4, nbformat_minor: 4 }该 fixture 没有任何代码块级断言测试通过的判据就是 lint 全流程不产生诊断、不 panic。它守护的是一个真实的边界条件当cell_offsets().content_ranges()为空没有可解析的 Python 区间时parse_cells_unchecked的“逐 cell 解析 合并”路径仍要正常完成而不是在空列表上出错或对纯 markdown notebook 报告无关诊断。场景三单元格边界 token 与跨 cell 的语义规则文档的第三个场景是技术含量最高的一组断言。它的前提是Per-cell parsing inserts tokens at each boundary before merging the cells into one module.逐 cell 解析会在每个边界处插入 token之后再把各 cell 合并为一个模块。这里的“边界 token”指解析一个 cell 时为了正确收束缩进语法而补齐的词法单元如DedentPython 的缩进状态是跨行的独立解析一个 cell 必须在结束前把缩进栈压平。这个场景同时压测三类跨 cell 交互并断言所有选中的规则都不应报告任何诊断结尾 Dedentcell 以缩进块结束时边界处补齐的Dedent不应触发空行类规则跨 cell 的定义引用compute在 cell 1 定义、cell 3 使用合并模块必须让语义分析F821未定义名看到这一引用跨 cell 的 range 抑制# ruff: disable[F401]指令在 cell 2、被抑制的import json在 cell 3抑制范围跨越 cell 边界依然生效。选中规则列表文档中的[TOML]配置块完整继承[lint] select [ E301, E302, E303, E305, E306, F401, F821, W291, W293, W391, ]规则覆盖面经过精心设计E301/E302/E303/E305/E306是 pycodestyle 的空行/缩进类规则验证边界 token 不产生伪空行或伪缩进问题W291/W293检查行尾空白验证 cell 拼接处不引入尾随空白W391检查文件末尾多余空行验证合并模块的末尾 token 序列干净F401/F821验证跨 cell 语义。fixtureboundary-tokens.ipynb完整内嵌文件{ cells: [ { cell_type: code, execution_count: null, metadata: {}, outputs: [], source: [def compute():\n, return 1] }, { cell_type: code, execution_count: null, metadata: {}, outputs: [], source: [# ruff: disable[F401]] }, { cell_type: code, execution_count: null, metadata: {}, outputs: [], source: [import json\n, print(compute())] } ], metadata: {}, nbformat: 4, nbformat_minor: 4 }逐 cell 对照三类压测点cell 1 的def compute():块在边界处以补齐的Dedent收束cell 2 只有抑制指令# ruff: disable[F401]cell 3 中import json应被 cell 2 的抑制覆盖否则报F401print(compute())引用 cell 1 的定义否则报F821。该 fixture 没有任何# snapshot断言按 mdtest 的匹配逻辑“零诊断”本身就是断言只要边界 token、跨 cell 语义或 range 抑制任何一处实现回退选中规则就会报错并使测试失败。如何运行与扩展这些测试这份文档的测试由ruff_mdtestcrate 的 harness 自动发现无需手工注册。在仓库根目录可以按测试名过滤运行# 运行 cell-boundaries.md 这一个 mdtest fixture cargo test -p ruff_mdtest --test mdtest -- cell-boundariesmdtest 框架同时提供两个环境变量定义于 mdtest 库MDTEST_TEST_FILTER用于按名称过滤要执行的测试MDTEST_UPDATE_SNAPSHOTS设为非0值用于自动回写snapshot内联快照。若要为 notebook 解析新增边界用例只需在crates/ruff_linter/resources/mdtest/notebook/下新增一个.md按现有格式写明[TOML]配置块与ipynb内嵌文件harness 支持的块语言为py/python、pyi、ipynb、toml与ignore见 ruff_mdtest/src/lib.rs 的assert_matches!harness 会将其纳入同一套“写入内存/src→ lint → 断言匹配”的流程。小结cell-boundaries.md 用三个紧凑的 fixture 精确刻画了 ruff notebook 解析的三条不变量错误不越过 cell 边界、无 Python 单元格可安全解析、边界 token 不影响空行/缩进/抑制类规则的判断。这些不变量最终都收敛到 linter.rs 中的parse_cells_unchecked分派 这一处实现逐 cell 的词法/语法解析保证错误定位与缩进收束的局部性合并模块保证语义规则的跨 cell 全局性。文档既是行为规格也是回归测试本身——任何破坏这三条不变量的改动都会让对应的 mdtest 用例直接失败。【免费下载链接】ruffAn extremely fast Python linter and code formatter, written in Rust.项目地址: https://gitcode.com/GitHub_Trending/ru/ruff创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表