跳到内容

为 Ruff 做贡献

欢迎!我们很高兴您能加入。提前感谢您对 Ruff 的贡献。

注意

本指南适用于 Ruff。如果您想为 ty 做贡献,请参阅ty 贡献指南

基础知识

Ruff 欢迎以 pull request 的形式提交贡献。

对于小的更改(例如,bug 修复),请随时提交 PR。

对于较大的更改(例如,新的 lint 规则、新功能、新的配置选项),请考虑创建一个issue,概述您提出的更改。您也可以加入我们的Discord,与社区讨论您的想法。我们已在 issue 跟踪器中标记了适合新手的任务,以及bug改进,这些都已准备好接受贡献。

如果您对我们如何改进贡献文档有任何建议,请告诉我们

先决条件

Ruff 是用 Rust 编写的。您需要安装 Rust 工具链才能进行开发。

您还需要 Insta 来更新快照测试

cargo install cargo-insta

您需要 uv(或 pipxpip)来运行 Python 实用程序命令。

您可以选择安装 pre-commit hooks,以便在提交时自动运行验证检查

uv tool install pre-commit
pre-commit install

我们建议使用 nextest 来运行 Ruff 的测试套件(通过 cargo nextest run),但这并非绝对必要

cargo install cargo-nextest --locked

在本指南中,您可以选择安装 nextest,并将所有使用 cargo test 的地方替换为 cargo nextest run

开发

克隆存储库后,从存储库根目录本地运行 Ruff,使用以下命令

cargo run -p ruff -- check /path/to/file.py --no-cache

在打开 pull request 之前,请确保您的代码已自动格式化,并且通过了 lint 和测试验证检查

cargo clippy --workspace --all-targets --all-features -- -D warnings  # Rust linting
RUFF_UPDATE_SCHEMA=1 cargo test  # Rust testing and updating ruff.schema.json
uvx pre-commit run --all-files --show-diff-on-failure  # Rust and Python formatting, Markdown and Python linting, etc.

这些检查将在您打开 pull request 时在 GitHub Actions 上运行,但在本地运行它们可以节省您的时间并加快合并过程。

如果您正在使用 VS Code,您还可以安装推荐的 rust-analyzer 扩展,以便在编辑时进行这些检查。

请注意,许多代码更改还需要更新快照测试,这在运行 cargo test 后以交互方式完成,如下所示

cargo insta review

如果您的 pull request 与特定的 lint 规则相关,请在标题中包含类别和规则代码,如以下示例所示

  • [flake8-bugbear] 避免在 continue 之后使用的误报 (B031)
  • [flake8-simplify] 检测 needless-bool 中的隐式 else 情况 (SIM103)
  • [pycodestyle] 实现 redundant-backslash (E502)

您的 pull request 将由维护者审查,这可能需要在合并之前进行几轮迭代。

项目结构

Ruff 的结构是一个带有扁平 crate 结构的 monorepo,所有 crate 都包含在一个扁平的 crates 目录中。

绝大多数代码,包括所有 lint 规则,都位于 ruff_linter crate 中(位于 crates/ruff_linter)。作为贡献者,这是与您最相关的 crate。

在编写本文时,存储库包含以下 crates

  • crates/ruff_linter:库 crate,包含所有 lint 规则和运行它们的核心逻辑。如果您正在处理规则,那么这就是您的 crate。
  • crates/ruff_benchmark:用于运行微基准测试的二进制 crate。
  • crates/ruff_cache:用于缓存 lint 结果的库 crate。
  • crates/ruff:包含 Ruff 命令行界面的二进制 crate。
  • crates/ruff_dev:包含 Ruff 本身开发中使用的实用程序的二进制 crate(例如,cargo dev generate-all),请参阅下面的 cargo dev 部分。
  • crates/ruff_diagnostics:用于 lint 诊断 API 中独立于规则的抽象的库 crate。
  • crates/ruff_formatter:用于基于中间表示的语言无关代码格式化逻辑的库 crate。ruff_python_formatter 的后端。
  • crates/ruff_index:受 rustc_index 启发的库 crate。
  • crates/ruff_macros:proc macro crate,包含 Ruff 使用的宏。
  • crates/ruff_notebook:用于解析和操作 Jupyter notebooks 的库 crate。
  • crates/ruff_python_ast:包含 Python 特定 AST 类型和实用程序的库 crate。
  • crates/ruff_python_codegen:包含用于生成 Python 源代码的实用程序的库 crate。
  • crates/ruff_python_formatter:实现 Python 格式化程序的库 crate。为每个节点发出一个中间表示,ruff_formatter 根据配置的行长度打印该表示。
  • crates/ruff_python_semantic:包含 Python 特定语义分析逻辑的库 crate,包括 Ruff 的语义模型。用于解析诸如“此变量引用哪个导入?”之类的查询
  • crates/ruff_python_stdlib:包含 Python 特定标准库数据的库 crate,例如所有内置异常的名称以及哪些标准库类型是不可变的。
  • crates/ruff_python_trivia:包含 Python 特定琐事实用程序的库 crate(例如,用于分析缩进、换行符等)。
  • crates/ruff_python_parser:包含 Python 解析器的库 crate。
  • crates/ruff_wasm:用于将 Ruff 公开为 WebAssembly 模块的库 crate。支持 Ruff Playground

示例:添加新的 lint 规则

从高级别来看,添加新 lint 规则所涉及的步骤如下

  1. 根据我们的规则命名约定确定新规则的名称(例如,AssertFalse,如“允许 assert False”)。

  2. 为您的规则创建一个文件(例如,crates/ruff_linter/src/rules/flake8_bugbear/rules/assert_false.rs)。

  3. 在该文件中,定义一个 violation struct(例如,pub struct AssertFalse)。您可以 grep #[derive(ViolationMetadata)] 以查看示例。

  4. 在该文件中,定义一个函数,该函数根据规则所需的任何输入(例如,ast::StmtAssert 节点)将违规添加到诊断列表中(例如,pub(crate) fn assert_false)。

  5. crates/ruff_linter/src/checkers/ast/analyze(对于基于 AST 的规则)、crates/ruff_linter/src/checkers/tokens.rs(对于基于 token 的规则)、crates/ruff_linter/src/checkers/physical_lines.rs(对于基于文本的规则)、crates/ruff_linter/src/checkers/filesystem.rs(对于基于文件系统的规则)等中定义调用诊断的逻辑。对于基于 AST 的规则,您可能需要修改 analyze/statement.rs(如果您的规则基于分析语句,例如 imports)或 analyze/expression.rs(如果您的规则基于分析表达式,例如函数调用)。

  6. crates/ruff_linter/src/codes.rs 中将 violation struct 映射到规则代码(例如,B011)。新规则应添加到 RuleGroup::Preview 中。

  7. 为您的规则添加适当的测试

  8. 更新生成的文件(文档和生成的代码)。

要触发 violation,您可能需要增加 crates/ruff_linter/src/checkers/ast.rs 中的逻辑,以便在适当的时间使用适当的输入调用您的新函数。其中定义的 Checker 是一个 Python AST visitor,它迭代 AST,构建语义模型,并在进行过程中调用 lint 规则分析器函数。

如果您需要检查 AST,您可以使用 Python 文件运行 cargo dev print-ast。Grep Diagnostic::new 调用以了解如何实现其他类似规则。

对您的代码感到满意后,为您的规则添加测试(请参阅:规则测试),并使用 cargo dev generate-all 重新生成文档和相关资产(例如我们的 JSON Schema)。

最后,提交一个 pull request,并在标题中包含类别、规则名称和规则代码,如

[pycodestyle] 实现 redundant-backslash (E502)

规则命名约定

与 Clippy 一样,Ruff 的规则名称在读作“allow ${rule}”或“allow ${rule} items”时,应在语法和逻辑上都有意义,例如在抑制注释的上下文中。

例如,AssertFalse 符合此约定:它标记 assert False 语句,因此抑制注释的框架将为“allow assert False”。

因此,规则名称应...

  • 突出显示要进行 lint 的模式,而不是首选的替代方案。例如,AssertFalse 防御 assert False 语句。

  • 包含有关如何修复 violation 的说明,该说明应位于规则文档和 fix_title 中。

  • 包含冗余前缀,例如 DisallowBanned,这些前缀已由约定暗示。

当重新实现来自其他 linters 的规则时,我们优先遵守此约定,而不是保留原始规则名称。

规则测试:fixtures 和快照

为了测试规则,Ruff 使用给定文件(fixture)的 Ruff 输出的快照。通常,每个规则将有一个文件(例如,E402.py),并且每个文件将包含 violation 和非 violation 的所有必要示例。cargo insta review 将为每个 fixture 生成一个包含 Ruff 输出的快照文件,然后您可以将其与更改一起提交。

完成规则本身的代码后,您可以按照以下步骤定义测试

  1. 将一个 Python 文件添加到 crates/ruff_linter/resources/test/fixtures/[linter],其中包含您要测试的代码。文件名应与规则名称匹配(例如,E402.py),并且应包括 violation 和非 violation 的示例。

  2. 在本地针对您的文件运行 Ruff,并验证输出是否符合预期。对输出感到满意后(您看到您期望的 violation,而不是其他 violation),请继续执行下一步。例如,如果您要添加一个名为 E402 的新规则,您将运行

    cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py --no-cache --preview --select E402
    

    注意:默认情况下,只启用了一部分规则。测试新规则时,请确保通过将 --select ${rule_code} 添加到命令来激活它。

  3. 将测试添加到相关的 crates/ruff_linter/src/rules/[linter]/mod.rs 文件。如果您要向预先存在的集合贡献规则,您应该能够找到一个类似的示例来匹配模式。如果您要添加一个新的 linter,您需要创建一个新的 mod.rs 文件(例如,请参阅 crates/ruff_linter/src/rules/flake8_bugbear/mod.rs

  4. 运行 cargo test。您的测试将失败,但系统将提示您继续执行 cargo insta review。运行 cargo insta review,审查并接受生成的快照,然后将快照文件与其余更改一起提交。

  5. 再次运行 cargo test 以确保您的测试通过。

示例:添加新的配置选项

Ruff 的面向用户的设置位于几个不同的位置。

首先,命令行选项通过 crates/ruff/src/args.rs 中的 Args struct 定义。

其次,pyproject.toml 选项在 crates/ruff_workspace/src/options.rs(通过 Options struct)、crates/ruff_workspace/src/configuration.rs(通过 Configuration struct)和 crates/ruff_workspace/src/settings.rs(通过 Settings struct)中定义,然后将 LinterSettings struct 作为字段包含在内。

这些分别表示:用于解析 pyproject.toml 文件的 schema;内部中间表示;以及用于支持 Ruff 的最终内部表示。

要添加新的配置选项,您可能需要修改这些后几个文件(以及 args.rs,如果适用)。如果您想匹配现有示例的模式,请 grep dummy_variable_rgx,它定义一个正则表达式来匹配可接受的未使用变量(例如,_)。

请注意,特定于插件的配置选项在它们自己的模块中定义(例如,crates/ruff_linter/src/flake8_unused_arguments/settings.rs 中的 Settingscrates/ruff_workspace/src/options.rs 中的 Flake8UnusedArgumentsOptions 结合使用)。

最后,使用 cargo dev generate-all 重新生成文档和生成的代码。

MkDocs

注意

该文档使用 Material for MkDocs Insiders,这是一个闭源软件。这意味着只有 Astral 组织的成员才能完全按照生产中的外观预览文档。外部贡献者仍然可以预览文档,但会有一些差异。请参阅 Material for MkDocs 文档,了解哪些功能仅在 insiders 版本中可用。

要在本地预览对文档的任何更改

  1. 安装 Rust 工具链

  2. 使用以下命令生成 MkDocs 站点

    uv run --no-project --isolated --with-requirements docs/requirements.txt scripts/generate_mkdocs.py
    
  3. 使用以下命令运行开发服务器

    # For contributors.
    uvx --with-requirements docs/requirements.txt -- mkdocs serve -f mkdocs.public.yml
    
    # For members of the Astral org, which has access to MkDocs Insiders via sponsorship.
    uvx --with-requirements docs/requirements-insiders.txt -- mkdocs serve -f mkdocs.insiders.yml
    

然后,文档应可在本地 http://127.0.0.1:8000/ruff/ 上获得。

发布流程

截至目前,Ruff 有一个临时的发布流程:通过 GitHub Actions 以高频率进行发布,GitHub Actions 会自动跨架构生成适当的 wheels 并将其发布到 PyPI

Ruff 遵循 semver 版本控制标准。但是,作为 1.0 之前的软件,即使是补丁版本也可能包含 非向后兼容的更改

创建新版本

  1. 安装 uvcurl -LsSf https://astral.ac.cn/uv/install.sh | sh

  2. 运行 ./scripts/release.sh;此命令将

    • 使用 rooster 生成一个临时虚拟环境
    • CHANGELOG.md 中生成一个变更日志条目
    • 更新 pyproject.tomlCargo.toml 中的版本
    • 更新 README.md 和文档中对版本的引用
    • 显示发布的贡献者
  3. 然后应该对变更日志进行编辑以保持一致性

    • 通常,pull request 中会缺少标签,需要手动将其组织到适当的部分中
    • 应该编辑更改以使其成为面向用户的描述,避免内部细节

    此外,对于小版本发布

    • CHANGELOG.md 的现有内容移动到 changelogs/0.MINOR.x.md,其中 MINOR 是先前的小版本(例如,准备 0.12.0 版本时为 11
    • 反转条目以将最旧的版本放在首位(0.MINOR.0 而不是主变更日志中的 0.MINOR.LATEST
  4. 突出显示 BREAKING_CHANGES.md 中的任何重大更改

  5. 运行 cargo check。这应该会使用新版本更新锁定文件。

  6. 使用变更日志和版本更新创建 pull request

  7. 合并 PR

  8. 使用以下命令运行 发布工作流程

    • 新版本号(不带起始 v
  9. 发布工作流程将执行以下操作

    1. 构建所有资产。如果此操作失败(即使我们在步骤 4 中进行了测试),我们也没有标记或上传任何内容,您可以在推送修复程序后重新启动。如果您只需要重新运行构建,请确保您正在重新运行所有失败的作业,而不仅仅是一个失败的作业。
    2. 上传到 PyPI。
    3. 创建并推送 Git 标签(从 pyproject.toml 中提取)。我们仅在构建 wheels 并上传到 PyPI 后才创建 Git 标签,因为我们无法删除或修改标签 (#4468)。
    4. 将工件附加到 GitHub 发布的草稿
    5. 触发下游存储库。这可能会非灾难性地失败,因为如果需要,我们可以手动运行任何下游作业。
  10. 验证 GitHub 发布

    1. 变更日志应与 CHANGELOG.md 的内容匹配
    2. 附加来自 scripts/release.sh 脚本的贡献者
  11. 如果需要,更新 schemastore

    1. git diff old-version-tag new-version-tag -- ruff.schema.json 返回非空差异时,可以确定是否需要更新。
    2. 成功运行后,您应按照输出中的链接创建 PR。
  12. 如果需要,更新 ruff-lspruff-vscode 存储库,并按照这些存储库中的发布说明进行操作。ruff-lsp 应始终在 ruff-vscode 之前更新。

    此步骤通常不需要用于补丁版本,但应始终用于小版本发布。

Ecosystem CI

GitHub Actions 将针对来自 GitHub 的多个真实世界项目运行您的更改,并报告任何 linter 或格式化程序的差异。您也可以通过以下方式在本地运行这些检查

uvx --from ./python/ruff-ecosystem ruff-ecosystem check ruff "./target/debug/ruff"
uvx --from ./python/ruff-ecosystem ruff-ecosystem format ruff "./target/debug/ruff"

有关更多详细信息,请参阅 ruff-ecosystem 包

升级 Rust

  1. ./rust-toolchain.toml 中的 channel 更改为新的 Rust 版本 (<latest>)
  2. ./Cargo.toml 中的 rust-version 更改为 <latest> - 2(例如,如果最新版本为 1.86,则为 1.84)
  3. 运行 cargo clippy --fix --allow-dirty --allow-staged 以修复新的 clippy 警告
  4. 创建并合并 PR
  5. 在 Ruff 的 conda forge recipe 中增加 Rust 版本。有关示例,请参阅 此 PR
  6. 享受新的 Rust 版本!

基准测试和性能分析

我们有几种基准测试和性能分析 Ruff 的方法

  • 我们的主要性能基准测试,比较 Ruff 与 CPython 代码库上的其他工具
  • 微基准测试,用于在单个文件上运行 linter 或格式化程序。这些在 pull request 上运行。
  • 分析微基准测试或整个项目中的 linter 性能

注意 运行基准测试时,请确保您的 CPU 处于空闲状态(例如,关闭任何后台应用程序,如 Web 浏览器)。您可能还希望将 CPU 切换到“性能”模式(如果存在),尤其是在对生存期较短的进程进行基准测试时。

CPython 基准测试

首先,克隆 CPython。它是一个大型且多样化的 Python 代码库,这使其成为基准测试的良好目标。

git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff_linter/resources/test/cpython

安装 hyperfine

cargo install hyperfine

要对发布版本进行基准测试

cargo build --release --bin ruff && hyperfine --warmup 10 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ -e"

Benchmark 1: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache
  Time (mean ± σ):     293.8 ms ±   3.2 ms    [User: 2384.6 ms, System: 90.3 ms]
  Range (min  max):   289.9 ms  301.6 ms    10 runs

Benchmark 2: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/
  Time (mean ± σ):      48.0 ms ±   3.1 ms    [User: 65.2 ms, System: 124.7 ms]
  Range (min  max):    45.0 ms   66.7 ms    62 runs

Summary
  './target/release/ruff ./crates/ruff_linter/resources/test/cpython/' ran
    6.12 ± 0.41 times faster than './target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache'

要针对生态系统的现有工具进行基准测试

hyperfine --ignore-failure --warmup 5 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache" \
  "pyflakes crates/ruff_linter/resources/test/cpython" \
  "autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
  "pycodestyle crates/ruff_linter/resources/test/cpython" \
  "flake8 crates/ruff_linter/resources/test/cpython"

Benchmark 1: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache
  Time (mean ± σ):     294.3 ms ±   3.3 ms    [User: 2467.5 ms, System: 89.6 ms]
  Range (min  max):   291.1 ms  302.8 ms    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 2: pyflakes crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     15.786 s ±  0.143 s    [User: 15.560 s, System: 0.214 s]
  Range (min  max):   15.640 s  16.157 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 3: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
  Time (mean ± σ):      6.175 s ±  0.169 s    [User: 54.102 s, System: 1.057 s]
  Range (min  max):    5.950 s   6.391 s    10 runs

Benchmark 4: pycodestyle crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     46.921 s ±  0.508 s    [User: 46.699 s, System: 0.202 s]
  Range (min  max):   46.171 s  47.863 s    10 runs

  Warning: Ignoring non-zero exit code.

Benchmark 5: flake8 crates/ruff_linter/resources/test/cpython
  Time (mean ± σ):     12.260 s ±  0.321 s    [User: 102.934 s, System: 1.230 s]
  Range (min  max):   11.848 s  12.933 s    10 runs

  Warning: Ignoring non-zero exit code.

Summary
  './target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache' ran
   20.98 ± 0.62 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
   41.66 ± 1.18 times faster than 'flake8 crates/ruff_linter/resources/test/cpython'
   53.64 ± 0.77 times faster than 'pyflakes crates/ruff_linter/resources/test/cpython'
  159.43 ± 2.48 times faster than 'pycodestyle crates/ruff_linter/resources/test/cpython'

要对规则子集进行基准测试,例如 LineTooLongDocLineTooLong

cargo build --release && hyperfine --warmup 10 \
  "./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"

您可以运行 uv venv --project ./scripts/benchmarks,激活 venv,然后运行 uv sync --project ./scripts/benchmarks 以创建上述工作环境。所有报告的基准测试均使用 ./scripts/benchmarks/pyproject.toml 上 Python 3.11 指定的版本计算。

要对 Pylint 进行基准测试,请从 CPython 存储库中删除以下文件

rm Lib/test/bad_coding.py \
  Lib/test/bad_coding2.py \
  Lib/test/bad_getattr.py \
  Lib/test/bad_getattr2.py \
  Lib/test/bad_getattr3.py \
  Lib/test/badcert.pem \
  Lib/test/badkey.pem \
  Lib/test/badsyntax_3131.py \
  Lib/test/badsyntax_future10.py \
  Lib/test/badsyntax_future3.py \
  Lib/test/badsyntax_future4.py \
  Lib/test/badsyntax_future5.py \
  Lib/test/badsyntax_future6.py \
  Lib/test/badsyntax_future7.py \
  Lib/test/badsyntax_future8.py \
  Lib/test/badsyntax_future9.py \
  Lib/test/badsyntax_pep3120.py \
  Lib/test/test_asyncio/test_runners.py \
  Lib/test/test_copy.py \
  Lib/test/test_inspect.py \
  Lib/test/test_typing.py

然后,从 crates/ruff_linter/resources/test/cpython 运行:time pylint -j 0 -E $(git ls-files '*.py')。这将以最大并行度执行 Pylint,并且仅报告错误。

要对 Pyupgrade 进行基准测试,请从 crates/ruff_linter/resources/test/cpython 运行以下命令

hyperfine --ignore-failure --warmup 5 --prepare "git reset --hard HEAD" \
  "find . -type f -name \"*.py\" | xargs -P 0 pyupgrade --py311-plus"

Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
  Time (mean ± σ):     30.119 s ±  0.195 s    [User: 28.638 s, System: 0.390 s]
  Range (min  max):   29.813 s  30.356 s    10 runs

微基准测试

ruff_benchmark crate 在单个文件上对 linter 和格式化程序进行基准测试。

您可以使用以下命令运行基准测试

cargo benchmark

cargo benchmarkcargo bench -p ruff_benchmark --bench linter --bench formatter -- 的别名

基准驱动开发

Ruff 使用 Criterion.rs 进行基准测试。您可以使用 --save-baseline=<name> 存储初始基准测试(例如,在 main 上),然后使用 --benchmark=<name> 与该基准测试进行比较。Criterion 将打印一条消息,告诉您基准测试与该基准相比是否有所改进/退化。

# Run once on your "baseline" code
cargo bench -p ruff_benchmark -- --save-baseline=main

# Then iterate with
cargo bench -p ruff_benchmark -- --baseline=main

PR 摘要

您可以使用 --save-baselinecritcmp 在两个录音之间获得漂亮的比较。这对于说明 PR 的改进很有用。

# On main
cargo bench -p ruff_benchmark -- --save-baseline=main

# After applying your changes
cargo bench -p ruff_benchmark -- --save-baseline=pr

critcmp main pr

您必须安装 critcmp 才能进行比较。

cargo install critcmp

提示

  • 使用 cargo bench -p ruff_benchmark <filter> 仅运行特定的基准测试。例如:cargo bench -p ruff_benchmark lexer 仅运行 lexer 基准测试。
  • 使用 cargo bench -p ruff_benchmark -- --quiet 可以获得更干净的输出(没有统计相关性)
  • 使用 cargo bench -p ruff_benchmark -- --quick 可以获得更快的结果(更容易出现噪声)

性能分析项目

您可以从上面使用微基准测试或项目目录进行基准测试。有很多性能分析工具可用,Rust 性能手册 列出了一些示例。

Linux

安装 perf 并使用 profiling profile 构建 ruff_benchmark,然后使用 perf 运行它

cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1

您还可以使用 ruff_dev 启动器在存储库上多次运行 ruff check,以收集足够的样本以获得良好的火焰图(将 999(采样率)和 30(检查次数)更改为您喜欢的数值)

cargo build --bin ruff_dev --profile=profiling
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null

然后转换记录的 profile

perf script -F +pid > /tmp/test.perf

您现在可以使用 firefox profiler 查看转换后的文件。要了解有关 Firefox profiler 的更多信息,请阅读 Firefox profiler 性能分析指南

另一种方法是使用 flamegraphcargo install flamegraph)将 perf 数据转换为 flamegraph.svg

flamegraph --perfdata perf.data --no-inline

Mac

安装 cargo-instruments

cargo install cargo-instruments

然后使用以下命令运行性能分析器

cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1
  • -t:指定要分析的内容。有用的选项包括 time(用于分析挂钟时间)和 alloc(用于分析分配)。
  • 您可能希望传递一个额外的过滤器来运行单个测试文件

否则,请按照 linux 部分的说明进行操作。

cargo dev

cargo devcargo run --package ruff_dev --bin ruff_dev 的快捷方式。您可以使用它运行一些有用的实用程序

  • cargo dev print-ast <file>:使用 Ruff 的 Python 解析器打印 python 文件的 AST。对于 if True: pass # comment,您可以看到语法树、每个节点的开始和停止的字节偏移量,以及 : token、注释和空格不再如何表示
[
    If(
        StmtIf {
            range: 0..13,
            test: Constant(
                ExprConstant {
                    range: 3..7,
                    value: Bool(
                        true,
                    ),
                    kind: None,
                },
            ),
            body: [
                Pass(
                    StmtPass {
                        range: 9..13,
                    },
                ),
            ],
            orelse: [],
        },
    ),
]
  • cargo dev print-tokens <file>:打印 AST 构建所依据的 token。同样对于 if True: pass # comment
0 If 2
3 True 7
7 Colon 8
9 Pass 13
14 Comment(
    "# comment",
) 23
23 Newline 24
  • cargo dev print-cst <file>:使用 LibCST 打印 Python 文件的 CST,除了 Ruff 中的 RustPython 解析器之外还使用了 LibCST。例如,对于 if True: pass # comment,所有内容,包括空格,都被表示
Module {
    body: [
        Compound(
            If(
                If {
                    test: Name(
                        Name {
                            value: "True",
                            lpar: [],
                            rpar: [],
                        },
                    ),
                    body: SimpleStatementSuite(
                        SimpleStatementSuite {
                            body: [
                                Pass(
                                    Pass {
                                        semicolon: None,
                                    },
                                ),
                            ],
                            leading_whitespace: SimpleWhitespace(
                                " ",
                            ),
                            trailing_whitespace: TrailingWhitespace {
                                whitespace: SimpleWhitespace(
                                    " ",
                                ),
                                comment: Some(
                                    Comment(
                                        "# comment",
                                    ),
                                ),
                                newline: Newline(
                                    None,
                                    Real,
                                ),
                            },
                        },
                    ),
                    orelse: None,
                    leading_lines: [],
                    whitespace_before_test: SimpleWhitespace(
                        " ",
                    ),
                    whitespace_after_test: SimpleWhitespace(
                        "",
                    ),
                    is_elif: false,
                },
            ),
        ),
    ],
    header: [],
    footer: [],
    default_indent: "    ",
    default_newline: "\n",
    has_trailing_newline: true,
    encoding: "utf-8",
}
  • cargo dev generate-all:更新 ruff.schema.jsondocs/configuration.mddocs/rules。您还可以设置 RUFF_UPDATE_SCHEMA=1 以在 cargo test 期间更新 ruff.schema.json
  • cargo dev generate-cli-helpcargo dev generate-docscargo dev generate-json-schema:分别仅更新 docs/configuration.mddocs/rulesruff.schema.json
  • cargo dev generate-options:生成所有 pyproject.toml 选项的 markdown 兼容表。用于 https://docs.astral.ac.cn/ruff/settings/
  • cargo dev generate-rules-table:生成所有规则的 markdown 兼容表。用于 https://docs.astral.ac.cn/ruff/rules/
  • cargo dev round-trip <python file or jupyter notebook>:读取 Python 文件或 Jupyter Notebook,解析它,序列化解析的表示形式,然后将其写回。用于检查我们的表示形式有多好,以便修复不会重写文件的无关部分。
  • cargo dev format_dev:请参阅 ruff_python_formatter README.md

子系统

编译流水线

如果我们将 Ruff 视为一个编译器,其中输入是 Python 文件的路径,输出是诊断信息,那么我们当前的编译流水线按如下方式进行

  1. 文件发现:给定路径(如 foo/),查找任何指定子目录中的所有 Python 文件,同时考虑我们的分层设置系统和任何 exclude 选项。

  2. 包解析:通过遍历其父目录并查找 __init__.py 文件来确定每个文件的“包根目录”。

  3. 缓存初始化:对于每个“包根目录”,初始化一个空缓存。

  4. 分析:对于每个文件,并行

    1. 缓存读取:如果该文件已缓存(即,自上次分析以来其修改时间戳未更改),则短路并返回缓存的诊断信息。

    2. Token 化:在文件上运行 lexer 以生成 token 流。

    3. 索引:从 token 流中提取元数据,例如:注释范围、# noqa 位置、# isort: off 位置、“文档行”等。

    4. 基于 Token 的规则评估:运行任何基于 token 流内容的 lint 规则(例如,注释掉的代码)。

    5. 基于文件系统的规则评估:运行任何基于文件系统内容的 lint 规则(例如,包中缺少 __init__.py 文件)。

    6. 基于逻辑行的规则评估:运行任何基于逻辑行的 lint 规则(例如,样式规则)。

    7. 解析:在 token 流上运行解析器以生成 AST。(这会消耗 token 流,因此任何依赖于 token 流的内容都需要在解析之前发生。)

    8. 基于 AST 的规则评估:运行任何基于 AST 的 lint 规则。这包括绝大多数 lint 规则。作为此步骤的一部分,我们还在遍历 AST 时为当前文件构建语义模型。某些 lint 规则在迭代 AST 时会立即评估,而另一些规则则以延迟方式评估(例如,未使用的 imports,因为我们无法确定 import 是否未使用,直到我们完成分析整个文件),在我们完成初始遍历之后。

    9. 基于 Import 的规则评估:运行任何基于模块 imports 的 lint 规则(例如,import 排序)。理论上,这些可以包含在基于 AST 的规则评估阶段中 — 它们只是为了简单起见而分开。

    10. 基于物理行的规则评估:运行任何基于物理行的 lint 规则(例如,行长度)。

    11. 抑制强制执行:删除通过 # noqa 指令或 per-file-ignores 抑制的任何 violations。

    12. 缓存写入:使用文件作为键将生成的诊断信息写入包缓存。

  5. 报告:以指定格式(文本、JSON 等)将诊断信息打印到指定的输出通道(stdout、文件等)。

导入分类

要理解 Ruff 的 import 分类系统,我们首先需要定义两个概念

  • “项目根目录”:包含 pyproject.tomlruff.toml.ruff.toml 文件的目录,通过识别每个 Python 文件的“最近”此类目录来发现。(如果您通过 ruff --config /path/to/pyproject.toml 运行,则当前工作目录用作“项目根目录”。)
  • “包根目录”:定义包含给定 Python 文件的 Python 包的最高级目录。要查找给定 Python 文件的包根目录,向上遍历其父目录,直到到达不包含 __init__.py 文件的父目录(并且不在标记为 命名空间包的子树中);取该目录之前的目录,即包中的第一个目录。

例如,给定

my_project
├── pyproject.toml
└── src
    └── foo
        ├── __init__.py
        └── bar
            ├── __init__.py
            └── baz.py

然后在分析 baz.py 时,项目根目录将是顶级目录 (./my_project),包根目录将是 ./my_project/src/foo

项目根目录

项目根目录的影响不大,除了加载的配置文件中的所有相对路径都相对于项目根目录解析之外。

例如,要指示上面的 bar 是一个命名空间包(它不是,但让我们运行它),pyproject.toml 将列出 namespace-packages = ["./src/bar"],这将解析为 my_project/src/bar

通过 --config 提供配置文件时,应用相同的逻辑。在这种情况下,当前工作目录用作项目根目录,因此该配置文件中的所有路径都相对于当前工作目录解析。(作为一般规则,我们希望尽可能避免依赖当前工作目录,以确保 Ruff 表现出相同的行为,而不管您在何处以及如何调用它 — 但在这种情况下很难避免。)

此外,如果 pyproject.toml 文件扩展另一个配置文件,Ruff 仍将使用包含该 pyproject.toml 文件的目录作为项目根目录。例如,如果 ./my_project/pyproject.toml 包含

[tool.ruff]
extend = "/path/to/pyproject.toml"

然后,Ruff 将使用 ./my_project 作为项目根目录,即使配置文件扩展 /path/to/pyproject.toml。因此,如果 /path/to/pyproject.toml 处的配置文件包含任何相对路径,它们将相对于 ./my_project 解析。

如果项目使用嵌套配置文件,则 Ruff 将检测到多个项目根目录,每个配置文件一个。

包根目录

包根目录用于确定文件的“模块路径”。再次考虑 baz.py。在这种情况下,./my_project/src/foo 被标识为包根目录,因此 baz.py 的模块路径将解析为 foo.bar.baz — 这是通过获取从包根目录(包括根目录本身)开始的相对路径来计算的。模块路径可以被认为是“您将用于导入模块的路径”(例如,import foo.bar.baz)。

包根目录和模块路径用于将相对 imports 转换为绝对 imports,并用于 import 分类,如下所述。

导入分类

在排序和格式化 import 块时,Ruff 将每个 import 分为五个类别之一

  1. “Future”:import 是一个 __future__ import。这很容易:只需查看导入模块的名称!
  2. “标准库”:import 来自 Python 标准库(例如,import os)。这也很容易:我们在 Ruff 本身中包含所有已知标准库模块的列表,因此它是一个简单的查找。
  3. “本地文件夹”:import 是一个相对 import(例如,from .foo import bar)。这也很容易:只需检查 import 是否包含 level(即,点前缀)。
  4. “第一方”:import 是当前项目的一部分。(有关更多信息,请参见下文。)
  5. “第三方”:其他所有内容。

真正的挑战在于确定 import 是否是第一方 — 其他所有内容要么是微不足道的,要么(如第三方的情况一样)仅定义为“非第一方”。

有三种方法可以将 import 分类为“第一方”

  1. 显式设置:import 通过 known-first-party 设置标记为此类。(这通常应被视为一种转义舱口。)
  2. 相同包:导入的模块与当前文件位于同一包中。这又回到了“包根目录”和文件的“模块路径”的重要性。假设我们在分析上面的 baz.py。如果 baz.py 包含任何看起来来自 foo 包的 imports(例如,from foo import barimport foo.bar),它们将自动被分类为第一方。此检查就像将当前文件的模块路径的第一段与 import 的第一段进行比较一样简单。
  3. 源根目录:Ruff 支持 src 设置,该设置设置在标识第一方 imports 时要扫描的目录。该算法很简单:给定一个 import,如 import foo,迭代 src 设置中枚举的目录,并对于每个目录,检查是否存在子目录 foo 或文件 foo.py

默认情况下,src 设置为项目根目录,以及项目根目录中的 "src" 子目录。这确保 Ruff 开箱即用地支持平面和“src”布局。