为 marimo 添加 Lint 规则:规则系统架构、代码分配与完整实现指南
【免费下载链接】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 的内置 lint 系统用于在 notebook 运行前检测可能阻碍执行、引发运行时问题、影响格式规范或破坏 WASM/Pyodide 兼容性的各类问题,帮助用户写出更可靠、可复现的 notebook。本文基于仓库中的开发文档 adding_lint_rules.md,结合marimo/_lint目录下的真实源码与测试,完整讲解规则的严重级别体系、规则码分配规范、从类实现到注册、测试、文档生成、修复机制的全流程,让你能够按官方约定为 marimo 贡献一条新的 lint 规则。
规则系统概览:四个严重级别
marimo 的 lint 系统围绕四个严重级别组织,每个级别对应一个独立的规则码前缀,也对应marimo/_lint/diagnostic.py中Severity枚举的四个取值:
- Breaking(MB):阻止 notebook 执行的错误,如无法解析的单元格、重复定义、循环依赖、setup 单元格依赖问题等;
- Runtime(MR):可能引发运行时问题的情况;
- Formatting(MF):风格与格式问题,以及解析阶段捕获到的 stdout/stderr 输出;
- WASM(MW):面向 WASM/Pyodide notebook 的兼容性问题,默认关闭(opt-in)。
四个前缀在源码中的定义位于 diagnostic.py:
class Severity(Enum): """Severity levels for diagnostic errors.""" FORMATTING = "formatting" # prefix: MF0000 RUNTIME = "runtime" # prefix: MR0000 BREAKING = "breaking" # prefix: MB0000 WASM = "wasm" # prefix: MW0000从当前仓库源码可以确认,各严重级别的具体规则分布如下:
- Breaking(breaking/init.py):MB001
unparsable-cells、MB002multiple-definitions、MB003cycle-dependencies、MB004setup-cell-dependencies、MB005syntax-errors; - Runtime(runtime/init.py):MR001
self-import、MR002branch-expression、MR003reusable-definition-order、MR004private-import-alias; - Formatting(formatting/init.py):MF001
general-formatting、MF002parse-stdout、MF003parse-stderr、MF004empty-cells、MF005sql-parse、MF006misc-log、MF007markdown-dedent; - WASM(wasm/init.py):MW001
incompatible-imports、MW002unsafe-system-calls、MW003incompatible-packages。
规则码分配:M[severity][number]规范
规则码遵循固定模式:M[severity][number]。目前各分类的编码区间为:
- MB001–MB099:Breaking 规则
- MR001–MR099:Runtime 规则
- MF001–MF099:Formatting 规则
- MW001–MW099:WASM 兼容性规则(默认关闭)
添加新规则时的分配步骤:
- 确定严重级别:根据影响程度选择 Breaking、Runtime 或 Formatting(WASM 仅当问题与 WASM/Pyodide 兼容性相关时才使用);
- 查找下一个可用编号:检查对应分类中已存在的规则;
- 使用连续编号:例如 MB005、MB006……
作为参考,文档中给出的示例分配与当前仓库源码完全一致:MB001 对应 unparsable-cells、MB002 对应 multiple-definitions、MB003 对应 cycle-dependencies、MB004 对应 setup-cell-dependencies、MF001 对应 general-formatting、MF002 对应 parse-stdout、MF003 对应 parse-stderr。注意 MB005 在仓库中已分配给syntax-errors(见 breaking/init.py),因此如果你要新增 Breaking 规则,应从 MB006 开始编号。
规则基类:LintRule 与 UnsafeFixRule
所有规则都必须继承 base.py 中定义的LintRule抽象基类,它声明了五个必须由子类设置的类属性,以及一个抽象方法:
class LintRule(ABC): """Base class for lint rules.""" # Class attributes that must be set by subclasses code: str name: str description: str severity: Severity fixable: bool | Literal["unsafe"] @abstractmethod async def check(self, ctx: RuleContext) -> None: """Check notebook for violations of this rule using the provided context."""各属性含义:
code:规则码,如"MB005";name:kebab-case 风格的名字,如"syntax-errors";description:用于规则列表展示的简短描述;severity:Severity.BREAKING/RUNTIME/FORMATTING/WASM之一;fixable:False表示不可修复;True表示安全修复(通过重新序列化自动应用);"unsafe"表示不安全修复(会改动 notebook 结构,需--unsafe-fixes标志)。在基类中其类型被注解为bool | Literal["unsafe"],具体取值由子类决定。
若规则需要实现“不安全修复”(即变更 notebook 结构),则应继承UnsafeFixRule(base.py)并实现apply_unsafe_fix方法,其签名为:
def apply_unsafe_fix( self, notebook: NotebookSerialization, diagnostics: list[Diagnostic] ) -> NotebookSerialization:仓库中 empty_cells.py 的EmptyCellRule是这一模式的真实范例:它的fixable = "unsafe",apply_unsafe_fix会收集诊断中携带的cell_id集合,从NotebookSerialization中过滤掉这些单元格后返回新的序列化对象。
逐步实现:从规则类到文档生成
第一步:创建规则类
根据规则类别将规则文件放入对应目录:
- Breaking 规则:
marimo/_lint/rules/breaking/ - Runtime 规则:
marimo/_lint/rules/runtime/ - Formatting 规则:
marimo/_lint/rules/formatting/ - WASM 规则:
marimo/_lint/rules/wasm/
文档提供的规则模板如下(已结合源码中真实规则的结构校对):
# Copyright 2026 Marimo. All rights reserved. from __future__ import annotations from typing import TYPE_CHECKING from marimo._lint.diagnostic import Diagnostic, Severity from marimo._lint.rules.base import LintRule if TYPE_CHECKING: from marimo._lint.context import RuleContext class YourNewRule(LintRule): """MB005: Brief description of what this rule checks. Detailed explanation of what this rule does and why it's important. This should explain the technical details of how the rule works. ## What it does Clear, concise explanation of what the rule detects. ## Why is this bad? Explanation of why this issue is problematic: - Impact on notebook execution - Potential for bugs or confusion - Effect on reproducibility ## Examples **Problematic:** ```python # Example of code that violates this rule bad_code = "example" ``` **Solution:** ```python # Example of how to fix the violation good_code = "example" ``` ## References - [Understanding Errors](https://docs.marimo.io/guides/understanding_errors/) - [Relevant Guide](https://docs.marimo.io/guides/...) """ code = "MB005" # Your assigned code name = "your-rule-name" # Kebab-case name description = "Brief description for rule listings" severity = Severity.BREAKING # Or RUNTIME/FORMATTING fixable = False # True if rule can auto-fix issues async def check(self, ctx: RuleContext) -> None: """Implement your rule logic here.""" # Iterate through notebook cells for cell in ctx.notebook.cells: # Your detection logic here if self._detect_violation(cell): diagnostic = Diagnostic( message="Description of the specific violation", line=cell.lineno, column=cell.col_offset + 1, code=self.code, name=self.name, severity=self.severity, fixable=self.fixable, ) await ctx.add_diagnostic(diagnostic) def _detect_violation(self, cell) -> bool: """Helper method for detection logic.""" # Implement your specific detection logic return False几个需要结合源码理解的实现要点:
ctx.add_diagnostic会回填缺省字段。在 context.py 的RuleContext.add_diagnostic中,如果Diagnostic的code、name、severity、fixable为None,会自动从规则对象上补齐,filename则从 notebook 序列化对象获取。因此像UnparsableRule(unparsable.py)那样只传message、line、column也能得到完整诊断。Diagnostic支持行/列为列表(见 diagnostic.py 中line: int | list[int]、column: int | list[int]),一条诊断可以指向多个位置,并通过cell_id关联到具体单元格。- 诊断会按严重级别进入优先级队列。
LintContext.add_diagnostic(context.py)使用PRIORITY_MAP(BREAKING=0、RUNTIME=1、FORMATTING=2、WASM=3)配合单调递增计数器将诊断压入heapq堆,保证输出时更严重的问题排在最前。
第二步:注册规则
将规则加入对应分类的__init__.py注册表。以 Breaking 规则为例(marimo/_lint/rules/breaking/__init__.py):
from marimo._lint.rules.breaking.your_file import YourNewRule BREAKING_RULE_CODES: dict[str, type[LintRule]] = { "MB001": UnparsableRule, "MB002": MultipleDefinitionsRule, "MB003": CycleDependenciesRule, "MB004": SetupCellDependenciesRule, "MB005": YourNewRule, # Add your rule here } __all__ = [ # ... existing rules ... "YourNewRule", # Add to exports "BREAKING_RULE_CODES", ]真实的BREAKING_RULE_CODES字典见 breaking/init.py。运行时、格式化、WASM 分类的注册表同样位于各自目录的__init__.py中。
第三步:创建测试文件
a) 创建测试 notebook 文件
在tests/_lint/test_files/your_rule_name.py创建一份真实的 marimo notebook 测试文件:
import marimo __generated_with = "0.15.2" app = marimo.App() @app.cell def _(): # Code that should trigger your rule problematic_code = "example" return @app.cell def _(): # Additional test cases return if __name__ == "__main__": app.run()仓库中tests/_lint/test_files/下已有大量此类测试文件,如 multiple_definitions.py、syntax_errors.py、wasm_incompatible.py 等,可作为新测试文件的参照。
b) 添加快照测试
在tests/_lint/test_snapshot.py中添加快照测试,其真实写法与文档模板一致(见 test_snapshot.py):
def test_your_rule_snapshot(): """Test snapshot for your new rule.""" file = "tests/_lint/test_files/your_rule_name.py" with open(file) as f: code = f.read() notebook = parse_notebook(code, filepath=file) errors = lint_notebook(notebook) # Format errors for snapshot error_output = [] for error in errors: error_output.append(error.format()) snapshot("your_rule_name_errors.txt", "\n".join(error_output))其中parse_notebook来自marimo._ast.parse,lint_notebook来自测试工具 tests/_lint/utils.py,它会通过RuleEngine.create_default()创建默认规则引擎并同步执行全部规则。快照文件会被写入tests/_lint/snapshots/目录(如 syntax_errors.txt),用于回归保护。
c) 添加单元测试(推荐)
创建tests/_lint/test_your_rule.py编写更严格的单元测试:
import pytest from marimo._ast.parse import parse_notebook from marimo._lint.context import LintContext from marimo._lint.rules.breaking import YourNewRule class TestYourNewRule: """Test cases for YourNewRule.""" async def test_detects_violation(self): """Test that the rule detects violations correctly.""" code = """import marimo app = marimo.App() @app.cell def _(): # Code that should trigger the rule return """ notebook = parse_notebook(code) ctx = LintContext(notebook) rule = YourNewRule() await rule.check(ctx) diagnostics = await ctx.get_diagnostics() assert len(diagnostics) > 0 assert diagnostics[0].code == "MB005" assert diagnostics[0].severity == Severity.BREAKING async def test_no_false_positives(self): """Test that the rule doesn't trigger on valid code.""" code = """import marimo app = marimo.App() @app.cell def _(): # Valid code that should not trigger the rule return """ notebook = parse_notebook(code) ctx = LintContext(notebook) rule = YourNewRule() await rule.check(ctx) diagnostics = await ctx.get_diagnostics() assert len(diagnostics) == 0仓库中的真实单元测试可参考 test_reusable_definition_order.py、test_wasm_rules.py 等。
第四步:生成文档
marimo 的 lint 规则文档由脚本从规则 docstring 自动生成,运行:
uv run scripts/generate_lint_docs.py该脚本(scripts/generate_lint_docs.py)会静态解析marimo/_lint/rules/下所有规则类,提取code、name、description、severity、fixable与 docstring,并生成:
- 每个规则独立的文档页面(
docs/guides/lint_rules/rules/your_rule_name.md); - 更新后的规则总索引
docs/guides/lint_rules/index.md。
因此规则 docstring 的规范至关重要,后文“文档要求”一节会详述其必须包含的章节。
第五步:运行测试
# Run lint tests uv run --group test pytest tests/_lint # Run your specific test uv run --group test pytest tests/_lint/test_your_rule.py # Update snapshots if needed uv run --group test pytest tests/_lint/test_snapshots.py --snapshot-update规则实现指南
检测逻辑
- 优先使用 AST 分析而非字符串匹配;
- 充分利用
RuleContext提供的上下文(notebook、依赖图等); - 错误信息要具体,帮助用户理解确切的问题;
- 考虑边界情况,用各种代码模式进行测试。
RuleContext(context.py)除了add_diagnostic外还提供:get_graph()获取(惰性构建并缓存的)依赖图、contents获取被检查文件内容、notebook获取序列化对象、stdout/stderr获取加载期间捕获的输出、get_logs(rule_code)与get_logs_for_cell(cell_id, rule_code)获取按规则/单元格分组的日志记录,以及get_errors(key)获取按类别(如"SyntaxError"、"unhandled")分组的编译异常。
错误信息
- 描述性:说明问题是什么;
- 可操作:建议如何修复;
- 一致性:遵循既有规则的措辞模式。
好的例子:"Variable 'x' is defined in multiple cells";差的例子:"Multiple definition error"。
性能
- 避免在热路径上进行昂贵操作。注意
LintContext.get_graph()使用双重检查加锁(double-checked locking)构建并缓存DirectedGraph,多个规则共享同一个图实例,这就是“添加到 context 中以复用”的官方做法; - 在检查多个单元格时缓存结果(必要时加入 context);
- 尽早返回。
可修复性(Fixability)
- 安全修复(
fixable = True):通过重新序列化自动应用。在 linter.py 的fix流程中,会基于(可能被修改的)notebook 重新生成文件内容,并且只有在内容真正发生变化(忽略__generated_with差异)时才写回磁盘; - 不安全修复(
fixable = "unsafe"):会变更 notebook 结构,必须使用--unsafe-fixes标志才会应用。实现方式为:在规则类中实现async def apply_unsafe_fixes(self, notebook, diagnostics) -> Notebook并继承UnsafeFixRule基类(真实签名见 base.py,为apply_unsafe_fix(notebook, diagnostics))。Linter.fix会先按规则码收集所有fixable == "unsafe"的诊断,再对每条规则调用一次apply_unsafe_fix。
默认规则与 opt-in 规则
规则的全集与默认集在 rules/init.py 中定义:
# Rules enabled by default (excludes opt-in categories like WASM). DEFAULT_RULE_CODES: dict[str, type[LintRule]] = ( BREAKING_RULE_CODES | RUNTIME_RULE_CODES | FORMATTING_RULE_CODES ) # All known rules (including opt-in). Used when --select is provided. RULE_CODES: dict[str, type[LintRule]] = DEFAULT_RULE_CODES | WASM_RULE_CODES要点:
RULE_CODES是所有已知规则的全集;DEFAULT_RULE_CODES是未指定--select时默认启用的规则集;- 若想让某个类别默认关闭(如 WASM 规则),将它包含进
RULE_CODES但不放进DEFAULT_RULE_CODES即可; - 用户通过
marimo check --select MW或--select ALL显式启用。
规则选择逻辑在 rule_selector.py 的resolve_rules()中实现,其算法为:
- 若
config.select非空,则从全部规则(RULE_CODES)中按前缀匹配选取;否则使用DEFAULT_RULE_CODES; - 移除与
config.ignore前缀匹配的规则; - 按规则码排序后实例化返回。
前缀匹配规则(_matches_any_prefix):"ALL"匹配一切;否则按前缀匹配,如"MB"匹配"MB001","MF0"匹配"MF001","MF001"为精确匹配。
在 CLI 层面,cli.py 的check命令提供了对应选项:
--select:逗号分隔的规则码/前缀,替换配置,如--select MB,MR001;--ignore:逗号分隔的规则码/前缀,如--ignore MF004,MF007;--fix:就地更新文件(应用安全修复);--unsafe-fixes:启用可能改变代码行为的修复(如删除空单元格);--strict:存在警告时返回非零退出码;--ignore-scripts:忽略无法识别为 marimo notebook 的文件;--format full|json:诊断输出格式;- 未提供文件参数时默认对
**/*.py、**/*.md、**/*.qmd进行 lint。
注意check命令退出码逻辑:linter.errored(存在 Breaking 级问题或文件处理失败)时,或--strict且有修复/问题时,返回退出码 1。
常见实现模式
遍历所有单元格
async def check(self, ctx: RuleContext) -> None: for cell in ctx.notebook.cells: if self._check_cell(cell): # Create diagnostic使用依赖图
async def check(self, ctx: RuleContext) -> None: graph = ctx.get_graph() for cell_id, cell_data in graph.cells.items(): # Analyze dependencies依赖图模式被 Breaking 规则广泛使用(如CycleDependenciesRule、MultipleDefinitionsRule、SetupCellDependenciesRule,见 breaking/graph.py),因为循环依赖、重复定义等问题只有在完整的 cell 依赖图上才能准确判断。
捕获解析期日志
部分解析问题会产生日志警告或错误,规则可以挂钩被捕获的日志。LintContext在构建依赖图时会逐 cell 编译并捕获日志(context.py),通过ctx.get_logs()/ctx.get_logs_for_cell()可访问。仓库中的MiscLogRule(MF006)即基于此机制,未归属到具体规则的日志默认归入 MF006。
注意:不要故意添加 log 语句来触发 lint 规则。日志语句只应在确实提供有用上下文时添加,且仅限 notebook 启动阶段。
测试最佳实践
快照测试
- 纳入快照测试以获得回归保护;
- 使用能清晰演示规则的现实示例;
- 边界情况放在独立单元测试中。
快照文件存放于tests/_lint/snapshots/(如 syntax_errors.txt),通过tests.mocks.snapshotter生成与管理。
单元测试
- 测试正向用例(规则正确触发);
- 测试负向用例(无误报);
- 测试边界情况(空单元格、语法错误等);
- 测试一个 notebook 中的多个违规。
仓库中已有丰富的测试集覆盖这些维度,例如 test_lint_system.py、test_streaming_early_stopping.py 还额外验证了流式输出与提前停止(EarlyStoppingConfig,见 rule_engine.py,支持stop_on_breaking、stop_on_runtime、max_diagnostics、stop_on_first_of_severity)。
测试文件结构
tests/_lint/ ├── test_files/ # Test notebooks │ └── your_rule_name.py ├── snapshots/ # Expected outputs │ └── your_rule_name_errors.txt ├── test_your_rule.py # Unit tests └── test_snapshots.py # Snapshot tests文档要求
规则 docstring 是自动文档生成的唯一数据源,必须包含:
- 第一行:规则码与简短描述(如
MB001: Cell contains unparsable code.); - ## What it does:技术解释;
- ## Why is this bad?:影响说明;
- ## Examples:代码示例(问题代码与修复方案);
- ## References:相关文档链接。
真实的示例可参见 unparsable.py(MB001)与 empty_cells.py(MF004)的 docstring。文档系统会自动:
- 生成每个规则的独立页面;
- 更新规则总索引;
- 创建导航链接;
- 使用人类可读的文件名。
提交前检查清单
在提交规则前逐项确认:
- 规则码符合编号约定
- 已在对应
__init__.py中注册 - docstring 完整且包含所有必需章节
- 单元测试覆盖正向与负向用例
- 包含快照测试
- 文档能正确生成
- 所有 lint 测试通过
- 错误信息清晰且可操作
- 大 notebook 下性能可接受
参考实现与进阶模式
文档提供了三个可参考的真实实现方向:
- 简单规则:如检测语法错误的
SyntaxErrorRule(MB005,syntax_error.py),可对照学习最小规则骨架; - 带
--unsafe-fixes的规则:如通过删除空单元格来变更 notebook 结构的EmptyCellRule(MF004,empty_cells.py),展示了UnsafeFixRule.apply_unsafe_fix的完整实现——先从各诊断收集cell_id,再重建不含这些单元格的NotebookSerialization; - 带日志上下文的规则:解析问题导致的日志警告/错误可通过规则挂钩,如
StdoutRule(MF002)解析file.py:line: message格式的捕获 stdout(parsing.py)。
一条规则从创建到上线的完整调用链是:CLIcheck命令(cli.py)→resolve_lint_config合并--select/--ignore→run_check构建 Linter → 每个文件经_process_single_file解析为NotebookSerialization→ RuleEngine 并发执行所有规则的check→ 诊断通过RuleContext进入LintContext的优先级队列 → 流式输出或 JSON 汇总。理解这条链路有助于你在实现新规则时准确判断问题应该发生在哪个环节、诊断应如何产生与呈现。
【免费下载链接】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),仅供参考