任务名称 | Python 端引入 ruff 作为代码风格检查/自动修复工具 |
---|---|
提交作者 | @SigureMo |
提交时间 | 2023-03-05 |
版本号 | v0.2 |
依赖飞桨版本 | develop |
文件名 | 20230305_introducing_ruff.md |
不久前 Paddle 刚刚完成了 Flake8 代码风格检查工具的引入(Tracking issue: PaddlePaddle/Paddle#46039)、Python 2.7 相关代码退场、Python 3.5/3.6 相关代码退场(Tracking issue: PaddlePaddle/Paddle#46837)三项 Call for Contribution 任务,Python 端代码的风格、整洁性已经得到了极大的提高。但是在我们实践的过程中也遇到了很多问题,部分问题现在也没有解决。本 RFC 旨在引入一个新的 Linter Ruff,以解决我们前几个 Call for Contribution 任务中的遗留问题,并进一步利用 Ruff 中内置的 rules 来优化代码风格。
Ruff 是一个利用 Rust 编写的 Linter,拥有极佳的运行速度(10-100 倍于现有的 Linter)。它重新实现了 Flake8 内置的绝大多数 rules 和若干受欢迎的 Flake8 插件。特别的是,它为大多数 rules 提供了自动修复功能。
在引入 Flake8 的过程中,由于 Flake8 没有提供自动修复功能,所以在存量修复时往往需要借助一些其他工具来完成,比如我们在 F401 错误码的修复过程(存量修复)中主要使用了 autoflake,并在之后 [Tools] Add autoflake pre-commit hook to remove unused-imports 引入了 autoflake 以自动移除未使用的 import(增量自动修复)。因此如果引入 Ruff,则可以利用一个工具直接完成存量修复和增量拦截和增量自动修复的功能。
在 Python 旧版本退场系列任务的开发前期,Ruff 还是一个非常早期的项目,只实现了较少的 pyupgrade rules,而 pyupgrade 本身则因为不支持禁用掉某一条或几条 rule 而难以使用,因此当时大多使用的是手动修复 + 写代码转换脚本来完成的。随着 Ruff 连续几个月的开发,目前 pyupgrade 相应的 rules 已经完全实现(相关 tracking issue:Implement pyupgrade),因此引入 Ruff 一方面可以大大减轻后续的 Python 旧版本退场的清理工作,另一方面可以对增量进行控制。
在升级飞桨代码中使用 NumPy 1.20 数据类型的用法中,我们对 NumPy 的弃用用法已经进行了替换,但是由于没有控制增量,因此目前代码库中已经出现了少许增量。而 Ruff 近期也新增了 NumPy 相关的一些 rules,比如 NPY001 对应的就是对 NumPy 1.20 弃用的数据类型的检查,引入 Ruff 即可直接对增量进行控制。
以上即是我们在相关社区任务中遇到的一些问题,而引入 Ruff 即可同时解决以上多个问题。
引入 Ruff 工具,利用 Ruff 完成以下效果:
- 引入 Ruff 的 pyupgrade rules:可自动对旧版本遗留代码进行升级,一方面可以完成旧版本清理任务中未做的「增量控制」,另一方面可以降低未来旧版本清理时的成本(比如 4 个月后即将 EOL 的 Python 3.7);
- 引入 Ruff 实现的 Flake8 其余插件:如比较受欢迎的 flake8-bugbear、flake8-comprehensions;
- 引入 Ruff 实现的专有 rule:比如 NumPy 弃用类型别名的检测 NPY001,引入该 rule 即可避免升级飞桨代码中使用 NumPy 1.20 数据类型的用法出现增量;
- 利用 Ruff 的自动修复功能替换掉 autoflake:目前 autoflake 是仅仅为了自动 Flake8 F401 rule 而引入的,因此可替换掉以精简工具数量;
- 引入 Ruff 实现的 Pylint rule 来替代原有的 iScan Python 流水线中的 Pylint 功能(PFCC Call for contribution - IDEA:iScan 流水线退场)。
- 进一步规范 Python 端代码风格;
- 通过自动化的方式来将部分代码转换为更加高效的代码(如 flake8-comprehensions 相关 rules);
- 精简 Python 端代码风格检查工具数量;
- 部分 rule 可启用自动修复功能,避免手动修复增量,降低开发者解决 Linter Error 的成本;
- Ruff 速度很快,可以使开发者有着更好的开发体验。
Paddle 目前引入了 Black、isort、Flake8、autoflake 以及一个仅用于检查 Docstring 但是已经失效的 pylint 共五个工具用于 Python 端的代码风格监控。Black 和 isort 属于 Formatter,Flake8 则属于 Linter,autoflake 目前仅仅用于 Flake8 F401 rule 的自动修复。
Ruff 的目标是成为 Python 语言的全能型 Linter,因此 Ruff 实现了大多数现有主流 Linter 的功能,并实现了部分 Formatter 的功能,整体对比如下:
Flake8 | PyLint | Black | isort | Pyupgrade | Ruff | |
---|---|---|---|---|---|---|
速度 | 慢 | 非常慢 | 快 | 一般 | 一般 | 非常快 |
是否支持插件 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
支持的 rule 数量 | 默认包含 132 条 rules,可通过安装插件进一步扩展 | 默认包含 395 条 rules,可通过自定义插件进一步扩展 | 仅格式化 | 仅排序 | 包含 43 条 rules | 默认包含 500+ 条 rules,且在持续增长中 |
支持自动修复 | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ |
Ruff 虽然目前不支持插件,但 Ruff 现有的内置 rules 基本可以覆盖所有常见的需求。
此外,Ruff 对于用户来说拥有统一的配置和 CLI 选项,不必同时学习多个工具,使用一个工具即可完成多个工具的工作。
由于 Ruff 自开始开发以来时间还不长,因此使用 Ruff 的项目还不多,但目前正在高速增长着,在 GitHub 上已经拥有 9.5k Star,在 Python 社区非常受欢迎。目前 Ruff 已经被 pandas、Transformers (Hugging Face)、Diffusers (Hugging Face)、SciPy、Jupyter、Pylint 等知名项目所使用。
PyTorch 等深度学习框架尚未引入 Ruff,但是 PyTorch 已经引入了 Flake8,除了启用了默认的 pycodestyle、pyflakes、mccabe、还引入了 flake8-bugbear、flake8-comprehensions、flake8-executable、flake8-logging-format、flake8-coding、flake8-pyi(见 pytorch/pytorch - requirements-flake8.txt),其中 flake8-bugbear、flake8-comprehensions、flake8-executable、flake8-logging-format 均已被 Ruff 实现,flake8-pyi 正在开发中,flake8-coding 尚未被实现。此外,PyTorch 目前也在考虑引入 Ruff,见 [BE]: Add ruff to lintrunner - use for additional plugins like pyupgrade etc。
TensorFlow 的代码风格管控较为宽松,目前并未使用 Flake8 这类 Linter,但使用了 pylint 来规范代码风格,见 tensorflow/tensorflow - tensorflow/tools/ci_build/pylintrc。目前 Ruff 也已经实现了很多 Pylint 的 rules,可通过引入 Ruff 实现的 Pylint rules 来达到同样的效果。
同 Flake8,Ruff 也需要 ignore 部分文件,初始化的配置如下:
pyproject.toml
:
[tool.ruff]
exclude = [
"./build",
"./python/paddle/fluid/[!t]**",
"./python/paddle/fluid/tra**",
"./python/paddle/utils/gast/**",
"./python/paddle/fluid/tests/unittests/npu/**",
"./python/paddle/fluid/tests/unittests/mlu/**",
]
target-version = "py37"
select = []
并添加相应的 pre-commit hook:
.pre-commit-config.yaml
:
- repo: /~https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.254
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix, --no-cache]
测试 PR 见:[CodeStyle] initialize ruff config
根据「相关背景」和「功能目标」中的调研,我们可以引入 Pyupgrade(UP)、NumPy-specific rules(NPY001)、flake8-bugbear(B)、flake8-comprehensions(C4)、Pylint(PL)、Pyflakes(F401),其余 Rules 在后续调研后认为合适即可引入。
引入 Rules 所需要考察的要点如下:
- 调研存量修复是否方便,是否 Ruff 是否已经提供了自动修复功能,如果没有,存量是否较少可手动修复,否则并不适合引入;
- 调研该 rule 是否会引起性能的倒退,比如 UP038(isinstance-with-tuple)在 Python 3.10 上会引起性能下降,因此并不适合在 Python 3.10 上引入,仅仅适合在 Python 3.11 上引入;另外由于 UP031(printf-string-formatting) 会稍微引起性能下降,而 UP032 (f-string)则会引起性能提升,因此建议两个 rule 一起引入(即在合并成一个 PR);
- 调研该 rule 是否会引起代码风格的倒退,比如 UP015(redundant-open-modes)并不满足「Explicit is better than implicit」的原则(参考 removing "r" in open(..., "r") is not an upgrade、PEP 20 – The Zen of Python),因此不适合引入。
此外应该注意 Paddle 的动转静单测部分,部分测试 case 会依赖于某一特定语法结构,相关代码应该注意避免 lint 和 autofix,遇到相关问题应该在配置文件的 per-file-ignores
中配置以跳过。
虽然 Ruff 为大多数可以自动修复的 rule 都提供了自动修复的功能,但并不是所有的自动修复效果都是合理的,因此是否要使用该 rule 的自动修复功能也需要逐 rule 进行判断,对于不合理的情况,应该仅仅拦截错误,由开发者手动修复。
主要要注意以下几点:
- 该 rule 的自动修复是否可能导致代码语义的变化;
- 该 rule 的自动修复是否会引起代码风格的倒退,如 NPY001(numpy-deprecated-type-alias),该 rule 的自动修复会将
np.int
替换为int
以确保代码的语义不变,但是对于这种情况,使用合适的np.int32
或者np.int64
是更推荐的修复方式,因此该 rule 并不推荐使用自动修复功能;
- 在引入 F401 自动修复功能后可以从
.pre-commit-config.yaml
移除 autoflake,引入 PR 见 [Tools]Add autoflake pre-commit hook to remove unused-imports/var; - 在引入 UP010 rule 后可以从
tools/check_file_diff_approvals.sh
移除相关检查项,引入 PR 见 [CodeStyle] add CI script to prevent future import
pyupgrade(UP)- 16 条、2358 处:
$ ruff --select UP . --statistics
14 UP004 [*] Class `Event` inherits from `object`
6 UP005 [*] `assertEquals` is deprecated, use `assertEqual`
1 UP006 [*] Use `list` instead of `List` for type annotations
80 UP008 [*] Use `super()` instead of `super(__class__, self)`
16 UP009 [*] UTF-8 encoding declaration is unnecessary
3 UP010 [*] Unnecessary `__future__` import `print_function` for target Python version
2 UP012 [*] Unnecessary call to `encode` as UTF-8
151 UP015 [*] Unnecessary open mode parameters
51 UP018 [*] Unnecessary call to `str`
9 UP024 [*] Replace aliased errors with `OSError`
10 UP027 [*] Replace unpacked list comprehension with a generator expression
14 UP028 [*] Replace `yield` over `for` loop with `yield from`
279 UP030 [*] Use implicit references for positional format fields
162 UP031 [*] Use format specifiers instead of percent format
1335 UP032 [*] Use f-string instead of `format` call
225 UP034 [*] Avoid extraneous parentheses
Pylint(PL)- 17 条、7684 处:
$ ruff --select PL . --statistics
215 PLR5501 [ ] Consider using `elif` instead of `else` then `if` to remove one indentation level
13 PLC0414 [*] Import alias does not rename original package
6 PLC3002 [ ] Lambda expression called directly. Execute the expression inline instead.
2 PLR0206 [ ] Cannot have defined parameters for properties
2113 PLR0402 [*] Use `from paddle import nn` in lieu of alias
5 PLR0133 [ ] Two constants compared in a comparison, consider replacing `10 > 5`
80 PLR1701 [ ] Merge these isinstance calls: `isinstance(norm, (float, int))`
41 PLR1722 [*] Use `sys.exit()` instead of `exit`
2182 PLR2004 [ ] Magic value used in comparison, consider replacing 3 with a constant variable
177 PLW0603 [ ] Using the global statement to update `_g_amp_state_` is discouraged
83 PLW0602 [ ] Using global for `_g_amp_state_` but no assignment is done
46 PLR0911 [ ] Too many return statements (8/6)
1446 PLR0913 [ ] Too many arguments to function call (8/5)
469 PLR0912 [ ] Too many branches (20/12)
469 PLR0915 [ ] Too many statements (51/50)
336 PLW2901 [ ] Outer for loop variable `pair` overwritten by inner assignment target
1 PLE1205 [ ] Too many arguments for `logging` format string
NumPy-specific rules(NPY001)- 1 条、2 处:
$ ruff --select NPY001 . --statistics
2 NPY001 [*] Type alias `np.bool` is deprecated, replace with builtin type
pyflakes(F401):无存量
flake8-comprehensions(C4)- 14 条、750 处:
$ ruff --select C4 . --statistics
19 C400 [*] Unnecessary generator (rewrite as a `list` comprehension)
5 C401 [*] Unnecessary generator (rewrite as a `set` comprehension)
4 C402 [*] Unnecessary generator (rewrite as a `dict` comprehension)
22 C403 [*] Unnecessary `list` comprehension (rewrite as a `set` comprehension)
2 C404 [*] Unnecessary `list` comprehension (rewrite as a `dict` comprehension)
172 C405 [*] Unnecessary `list` literal (rewrite as a `set` literal)
355 C408 [*] Unnecessary `tuple` call (rewrite as a literal)
3 C409 [*] Unnecessary `list` literal passed to `tuple()` (rewrite as a `tuple` literal)
8 C410 [*] Unnecessary `list` literal passed to `list()` (remove the outer call to `list()`)
6 C411 [*] Unnecessary `list` call (remove the outer call to `list()`)
1 C413 [*] Unnecessary `reversed` call around `sorted()`
17 C414 [*] Unnecessary `list` call within `sorted()`
104 C416 [*] Unnecessary `list` comprehension (rewrite using `list()`)
32 C417 [*] Unnecessary `map` usage (rewrite using a `list` comprehension)
flake8-bugbear(B)- 17 条、1373 处:
$ ruff --select B . --statistics
1 B004 [ ] Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
9 B005 [ ] Using `.strip()` with multi-character strings is misleading the reader
196 B006 [ ] Do not use mutable data structures for argument defaults
801 B007 [*] Loop control variable `atype` not used within loop body
24 B008 [ ] Do not perform function call `fluid.global_scope` in argument defaults
59 B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
29 B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
34 B011 [*] Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`
7 B015 [ ] Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it.
1 B016 [ ] Cannot raise a literal. Did you intend to return it or raise an Exception?
14 B017 [ ] `assertRaises(Exception)` should be considered evil
7 B020 [ ] Loop control variable `data` overrides iterable it iterates
113 B023 [ ] Function definition does not bind loop variable `opt_step`
1 B024 [ ] `FLClientBase` is an abstract base class, but it has no abstract methods
9 B026 [ ] Star-arg unpacking after a keyword argument is strongly discouraged
3 B027 [ ] `AlgorithmBase.collect_model_info` is an empty method in an abstract base class, but has no abstract decorator
65 B904 [ ] Within an except clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
其中标记有 [*]
的表示 Ruff 提供自动修复功能
[CodeStyle][pyupgrade] automatically rewrite code with ruff 已经尝试了引入 Ruff 的全部 pyupgrade rules(UP),可以通过全量单测。
第一个 PR 会修改配置,引入部分没有存量的 rule,之后由外部开发者提交 PR 来逐步引入有存量的 rule。具体实施步骤如下:
首先可按照「确定需要引入的 rules」和「确定该 rule 是否使用 Ruff 提供的自动修复功能」两小节确定是否引入该 rule 以及是否使用该 rule 的自动修复功能,如果确定引入该 rule 则在 pyproject.toml
的 Ruff 配置部分添加该 rule 对应的 violation,如添加 UP010
:
[tool.ruff]
# ...
- select = []
+ select = ["UP010"]
如果 Ruff 为该错误码提供自动修复方案且自动修复方案合适,之后在 Paddle 项目根目录运行 ruff . --fix
自动修复即可,否则需要 ruff .
后手动修复。
对于引入但不引入自动修复功能的 rule,需要在配置项 unfixable
中添加该 rule 的 violation,如 NPY001
:
[tool.ruff]
# ...
select = ["NPY001"]
- unfixable = []
+ unfixable = ["NPY001"]
对于多个存量较少的 rule 可以合并为一个 PR 提交,但尽可能不要超过 20 个文件的修改量。
建议使用单独的 PR 来修改配置,以避免 PR 频繁冲突。但可以将多个 rule 的配置 PR 合并成为一个。
不会对模块接口产生影响。
确保不会引起性能倒退,确保不会引起代码风格倒退,通过 CI 各条流水线。
用户对于框架内部代码风格的变动不会有任何感知,不会有任何影响。
可以提高 Paddle 代码风格,极大提高开发体验。
在 pre-commit 工作流中引入 Ruff,因此在该 hook 引入后开发者首次 commit 需要稍微等一段时间用于初始化 Ruff 环境,后续提交代码不受影响。
在确保 Rule 是安全的情况下,对性能不会产生任何影响,部分 Rule 可能会提高性能。
引入 Ruff 的 flake8-bugbear、flake8-comprehensions rules 可以对齐 PyTorch 的代码风格,引入 Ruff 的 Pylint rules 可以对齐 TensorFlow 的代码风格。在完成本 RFC 中所述的全部 rules 后,Paddle 的代码风格管控将会超越 TensorFlow 和 PyTorch。
Ruff 本身还处于早期阶段,因此部分选项和 rule 的作用可能会在未来变动,但在锁版本的情况下不存在该问题(pre-commit 配置中强制锁版本)。
关于用于更新 Ruff 版本的成本,只需要偶尔更新即可(比如一个月),并在更新版本的时候仔细阅读 Release Note 来确定升级方案,不需要频繁更新,维护成本并不高。
相关链接:Ruff 作者关于 stable 版本的回复 Open Version 1 road map, or whatever you decide to call “stable”… (comment)
任务 | 存量 | 预计完成时间 | 备注 |
---|---|---|---|
Ruff 配置初始化 | - | 1 人 1 天 | @SigureMo PR #51201 |
NumPy-specific rules(NPY001) | 存量 1 条、2 处 | 1 人 1 天 | |
pyupgrade(UP) | 存量 16 条、2358 处 | 1 人 1 周 | (测试 PR 见 PR #50477,实际合入拆分成多个 PR 以便 review) |
pyflakes(F401) | 存量 14 条、750 处 | 1 人 1 天 | |
flake8-comprehensions(C4) | 存量 14 条、750 处 | 1 人 1 周 | |
flake8-bugbear(B) | 存量 17 条、1373 处 | 1 人 2 周 | |
Pylint(PL) | 存量 17 条、7684 处 | 2 周 | |
6 月更新 Ruff,清理存量 | - | 1 人 2 天 | |
9 月更新 Ruff,清理存量 | - | 1 人 2 天 | |
12 月更新 Ruff,清理存量 | - | 1 人 2 天 |
待 Ruff 稳定后(0.1),可按照版本号来更新 Ruff,比如 0.1、0.2、0.3 等。
上表以优先级排序,不代表实际完成顺序,可并行进行。具体执行将会由 SigureMo 和外部开发者一起完成。
我们已经对引入 pyupgrade 进行了测试(见 [CodeStyle][pyupgrade] automatically rewrite code with pyupgrade),但 pyupgrade 不提供选项来禁用某一个或多个 rule,这意味着只能全盘接受或者不使用,而部分 rule 对于代码风格并不是提升,因此不会选择,另外 PyTorch 社区因为同样的原因没有选择引入 pyupgrade(见 Option to disable Unpacking list comprehensions
),因此也在考虑利用 Ruff 来引入 pyupgrade 的 rules。
Flake8 不提供自动修复功能,而 Ruff 可以尽可能地提供自动修复功能,可以同时减少引入时存量修复的工作量和之后开发者引入增量时修复的工作量。
目前 Ruff 实现的功能尚不能完全完成这三个插件的功能,部分社区在直接使用 Ruff 替换掉原有的 Flake8 后引起了代码风格回归的问题,见 ENH: Added analytical formula for truncnorm entropy - discussion,因此在 Ruff 完全实现 Flake8 内置插件全部 rules 且功能稳定之前,不会考虑直接使用 Ruff 替代这三个内置插件。
Ruff 目前同样实现了 isort 和 black 的功能,但这些功能实现尚处于早期,甚至还在持续开发中,因此现阶段不会考虑引入,日后如果 Ruff 的这两项功能稳定,且成为 Python 社区的主流解决方案之后,将会考虑使用 Ruff 直接替代这两项功能。
- rule:规则,即对应于某一类错误的检查项,如
UP010
检查不必要的 future import。