環境
$ ruff --version
ruff 0.0.264
$ flake8 --version
6.0.0 (mccabe: 0.7.0, pycodestyle: 2.10.0, pyflakes: 3.0.1) CPython 3.11.2 on Linux
$ pylint --version
pylint 2.17.3
astroid 2.15.4
Python 3.11.2 (main, Mar 2 2023, 15:11:26) [GCC 11.3.0]
はじめに
PythonのリンターをFlake8からRuffに移行した際、1行の長さが長すぎないかのチェック("Line too long")の結果が微妙に異なっていました。
どのようなときに結果が異なるかを、Flake8とRuffに加えPylintで確認しました。
設定ファイル
行の最大長さを11文字としました。
[tool.ruff]
select = ["E501"]
line-length = 11
[tool.pylint]
max-line-length = 11
disable = "all"
enable = "line-too-long"
[flake8]
select = E501
max-line-length = 11
#!/bin/bash -x
LINT_FILE=$1
ruff check ${LINT_FILE}
flake8 ${LINT_FILE}
pylint ${LINT_FILE}
"Line too long"の結果
文字列の場合
"1234567890"
"12345678 0"
"12345678 0"
sample.py
のすべての行が12文字です。2行目は半角スペース、3行目は全角スペースを含んでいます。
$ ./lint.sh sample.py
+ ruff check sample.py
sample.py:2:12: E501 Line too long (12 > 11 characters)
sample.py:3:11: E501 Line too long (13 > 11 characters)
Found 2 errors.
+ flake8 sample.py
sample.py:1:12: E501 line too long (12 > 11 characters)
sample.py:2:12: E501 line too long (12 > 11 characters)
sample.py:3:12: E501 line too long (12 > 11 characters)
+ pylint sample.py
************* Module sample
sample.py:1:0: C0301: Line too long (12/11) (line-too-long)
sample.py:2:0: C0301: Line too long (12/11) (line-too-long)
sample.py:3:0: C0301: Line too long (12/11) (line-too-long)
------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
Ruffでは、11文字を超えていてもスペースを含まなければ、"Line too long"のエラーになりませんでした。
以下は、Ruffの判定処理です。行をこれ以上短くできない場合は、エラーにならないようです。
let mut chunks = line.split_whitespace();
let (Some(first_chunk), Some(second_chunk)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return None;
};
コメント行の場合
# "34567890"
# "345678 0"
# "345678 0"
sample1.py
のすべての行が12文字です。2行目は半角スペース、3行目は全角スペースを含んでいます。
$ ./lint.sh sample1.py
+ ruff check sample1.py
sample1.py:1:12: E501 Line too long (12 > 11 characters)
sample1.py:2:12: E501 Line too long (12 > 11 characters)
sample1.py:3:11: E501 Line too long (13 > 11 characters)
Found 3 errors.
+ flake8 sample1.py
sample1.py:2:12: E501 line too long (12 > 11 characters)
sample1.py:3:12: E501 line too long (12 > 11 characters)
+ pylint sample1.py
************* Module sample1
sample1.py:1:0: C0301: Line too long (12/11) (line-too-long)
sample1.py:2:0: C0301: Line too long (12/11) (line-too-long)
sample1.py:3:0: C0301: Line too long (12/11) (line-too-long)```
1行目は#
の後にスペースがあるため、Ruffではエラーになりました。
しかし、Flake8ではエラーになりませんでした。
以下は、Flake8(実際の処理はpycodestyle)の判定処理です。
docstringやコメント行にURLのような長い文字がが存在するケースでは、エラーにならないようです。
# Special case for long URLs in multi-line docstrings or
# comments, but still report the error when the 72 first chars
# are whitespaces.
chunks = line.split()
if ((len(chunks) == 1 and multiline) or
(len(chunks) == 2 and chunks[0] == '#')) and \
len(line) - len(chunks[-1]) < max_line_length - 7:
return
#
の後ろに半角スペースがないコメント行の場合
あまり見かけないスタイルではありますが、#
の直後に半角スペースが存在しないコメント行でも確認しました。
#"234567890"
#"2345678 0"
#"2345678 0"
$ ./lint.sh sample2.py
+ ruff check sample2.py
sample2.py:2:12: E501 Line too long (12 > 11 characters)
sample2.py:3:11: E501 Line too long (13 > 11 characters)
Found 2 errors.
+ flake8 sample2.py
sample2.py:1:12: E501 line too long (12 > 11 characters)
sample2.py:2:12: E501 line too long (12 > 11 characters)
sample2.py:3:12: E501 line too long (12 > 11 characters)
+ pylint sample2.py
************* Module sample2
sample2.py:1:0: C0301: Line too long (12/11) (line-too-long)
sample2.py:2:0: C0301: Line too long (12/11) (line-too-long)
sample2.py:3:0: C0301: Line too long (12/11) (line-too-long)
Ruffでは、1行目はスペースが含まれていないので1単語とみなされて、エラーになりませんでした。
Flake8では、1行目はエラーになりました。Flake8のソースコードを見ると、chunks = line.split()
でスペースで分割した後、最初のchunkが#
かどうかを判定しています。1行目はスペースが含まれていないため、chunk[0]
は#"234567890"
となり、chunks[0] == '#'
がTrueになるため、エラーになったようです。
docstringの場合
2行目、6行目、10行目は12文字です。
"""
1234567890ab
"""
"""
12345678 0aa
"""
"""
12345678 0ab
"""
$ ./lint.sh sample3.py
+ ruff check sample3.py
sample3.py:6:12: E501 Line too long (12 > 11 characters)
sample3.py:10:11: E501 Line too long (13 > 11 characters)
Found 2 errors.
+ flake8 sample3.py
sample3.py:6:12: E501 line too long (12 > 11 characters)
sample3.py:10:12: E501 line too long (12 > 11 characters)
+ pylint sample3.py
************* Module sample3
sample3.py:2:0: C0301: Line too long (12/11) (line-too-long)
sample3.py:6:0: C0301: Line too long (12/11) (line-too-long)
sample3.py:10:0: C0301: Line too long (12/11) (line-too-long)
------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
Flake8では、2行目はエラーになりません。Flake8のソースコードを見ると、len(chunks) == 1 and multiline
とい判定を行っています。この判定がTrueになるため、エラーにならないようです。
URLが存在する場合
1行目、2行目は11文字を超えています。
"https://qiita.com"
# "https://qiita.com"
$ ./lint.sh sample4.py
+ ruff check sample4.py
+ flake8 sample4.py
sample4.py:1:12: E501 line too long (19 > 11 characters)
+ pylint sample4.py
************* Module sample4
sample4.py:1:0: C0301: Line too long (19/11) (line-too-long)
sample4.py:2:0: C0301: Line too long (21/11) (line-too-long)
2行目は#
の後ろにスペースがありますが、Ruffではエラーになりません。Ruffは、URLで終わる行はエラーにならないようにしています。
// Do not enforce the line length for lines that end with a URL, as long as the URL
// begins before the limit.
let last_chunk = chunks.last().unwrap_or(second_chunk);
if last_chunk.contains("://") {
if width - last_chunk.width() <= limit {
return None;
}
}
マルチバイト文字を含む場合
以下のPythonファイルでは、1行目も2行目も12文字です。2行目にはマルチバイト文字が1文字含まれています。
"12345678 0"
"12345678 あ"
$ ./lint.sh sample5.py
+ ruff check sample5.py
sample5.py:1:12: E501 Line too long (12 > 11 characters)
sample5.py:2:12: E501 Line too long (13 > 11 characters)
Found 2 errors.
+ flake8 sample5.py
sample5.py:1:12: E501 line too long (12 > 11 characters)
sample5.py:2:12: E501 line too long (12 > 11 characters)
+ pylint sample5.py
************* Module sample5
sample5.py:1:0: C0301: Line too long (12/11) (line-too-long)
sample5.py:2:0: C0301: Line too long (12/11) (line-too-long)
------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
Ruffでは、2行目は13文字だとみなされました。Ruffは、マルチバイト文字を1文字として扱わないようです。
for c in line.chars() {
if width < limit {
start_offset += c.text_len();
}
width += c.width().unwrap_or(0);
}
Ruffのv0.0.260で、"Line too long"の計算が"character count"から"Unicode width"に変わりました。pycodestyleではなくblackに合わせたようです。
This PR changes the logic testing if a line exceeds the configured line-width to use the unicode-width rather than character count.
Changing this breaks compatibility with pycodestyle because they decided to stick with ASCII width (because PEP8 recommends ASCII source code). I think its worth diverging from pycodestyle because our formatter uses unicode width and black is adopting too
まとめ
Ruff, Flake8, Pylintで、"Line too long"のエラーの挙動を確認しました。
- Ruff
- 行が長くても、1単語ならばエラーにならない
- 行が長くても、行の最後がURLならばエラーにならない
- マルチバイト文字は1文字として扱わない
- Flake8
- docstringかどうか、コメント行かどうかで結果が変わる
- Pylint
- 単純に行の長さだけで判定している