Ruffによる`E501`を修正した後にblackでフォーマットすると、再度RuffによりE501が発生する

# 環境

```
$ ruff --version
ruff 0.0.264

$ black --version
black, 23.3.0 (compiled: yes)
Python (CPython) 3.11.2
```

# はじめに
Pythonのフォーマッターに[black](https://github.com/psf/black)、リンターに[Ruff](https://github.com/charliermarsh/ruff)を使用しています。
1行が長すぎる場合、Ruffは`E501`（Line too long）エラーを出力します。`E501`を修正した後blackでフォーマットすると、再度Ruffにより`E501`エラーが出力されるケースがありました。
その現象をまとめます。

なお、RuffのIssueで報告されている内容と同じ現象です。

https://github.com/charliermarsh/ruff/issues/3825#issuecomment-1536070835

# 設定ファイル
1行の最大長さを20文字とします。

```toml:pyproject.toml
[tool.ruff]
select = ["RUF100", "E501"]
line-length = 20

[tool.black]
line-length = 20
```

# どのような現象か？



## 1.最初の状態

```python:sample11.py
print("あい　うえお")
```

1行目は15文字です。全角文字を2文字としてカウントすると、21文字です。

```
$ black sample11.py 
All done! ✨ 🍰 ✨
1 file left unchanged.

$ ruff check sample11.py 
sample11.py:1:15: E501 Line too long (21 > 20 characters)
Found 1 error.
```

blackは「1行目は20文字を超えていない」と判断して、何もしません。しかし[Ruffは全角文字を2文字としてカウントする](https://github.com/charliermarsh/ruff/pull/3714)ため、「1行目は20文字を超えている」と判断して、E501エラーとします。

## 2.Ruffでnoqaコメントを付ける
E501エラーを無視するため、行末に`noqa`ディレクティブを付けます。

```
$ ruff check sample11.py --add-noqa
Added 1 noqa directive.
```

```python:sample11.py
print("あい　うえお")  # noqa: E501
```

1行目は29文字です。20文字を超えていて、かつ改行すれば20文字以下になるので、blackでフォーマット可能な状態になります。

```
$ black sample11.py --check
would reformat sample11.py

Oh no! 💥 💔 💥
1 file would be reformatted.
```

## 3.blackでフォーマット

```
$ black sample11.py 
reformatted sample11.py

All done! ✨ 🍰 ✨
1 file reformatted.
```

```python:sample11.py
print(
    "あい　うえお"
)  # noqa: E501
```

blackでフォーマットすると改行されて、すべての行が20文字以下になりました。


## 4.再度Ruffでチェック
再度Ruffでチェックすると、「`# noqa: E501`は不要」というエラー`RUF100`が発生しました。
`--fix`オプションを付けて、不要な`noqa`ディレクティブを取り除きます。

```
$ ruff check sample11.py
sample11.py:3:4: RUF100 [*] Unused `noqa` directive (unused: `E501`)
Found 1 error.
[*] 1 potentially fixable with the --fix option.

$ ruff check sample11.py --fix
Found 1 error (1 fixed, 0 remaining).
```


```python:sample11.py
print(
    "あい　うえお"
)
```

2行目は13文字です。全角文字を2文字とカウントすると、19文字なのでRuffのE501エラーは発生しません。


## 5.再度blackでフォーマット

```
$ black sample11.py 
reformatted sample11.py

All done! ✨ 🍰 ✨
1 file reformatted.
```


```python:sample11.py
print("あい　うえお")
```


blackでフォーマットすると1行になりました。改行を取り除いて1行にしても20文字以下になるためです。
そして、最初の状態に戻ってしまいました。


# 原因
以下の条件を全て満たしていると、「フォーマット→リンターによるチェック＆修正→フォーマット→リンターによるチェック＆修正→。。。」と、コードは常に変わってしまいます。

* (a) フォーマットツールにblackを使用している
* (b) リンターにRuffを使用している
* (c) Ruffはルール`E501`と`RU100`をチェックする
* (d) Ruffは1行の最大文字数を超えていると判断するが、blackは最大文字数を超えていないと判断する
* (e) 行末に`# noqa: E501`を付けると、blackは最大文字数を超えていると判断する
* (f) blackでフォーマットした後は、Ruffは1行の最大文字数を超えていないと判断する

ポイントは条件(d)です。Ruffが全角文字を1文字としてカウントしてくれれば、blackと"line-length"のカウント方法は同じになります。Ruffとblackの"line-length"の設定が一致しているならば、条件(d)が満たさせることはありません。
Flake8の"line-length"はblackと同じ（全角文字を1文字としてカウントする）なので、このようにコードが常に変化することはありませんでした。

## なぜRuffは全角文字を1文字としてカウントしないのか？
Ruffのv0.0.260から、`line-length`の計算が"character count"から"Unicode width"に変更されました。

https://github.com/charliermarsh/ruff/pull/3714

これは、blackの以下のPRに合わせたためです。以下のPRは、black v23.3.0でリリースされました。ただし"Unicode width"を使用するのはpreview版だけです。

https://github.com/psf/black/pull/3445

以下は、blackの"line-length"の計算方法をpreview版と通常版で分岐している箇所です。
https://github.com/psf/black/blob/ef6e079901d53a42dfae4ab10b081ce7a73a47b5/src/black/lines.py#L763


# 解決策
## blackのpreview版を利用する
最初の状態のPythonファイルに対して、`--preview`オプションを指定してblackを実行すると、以下の状態になります。

```
5$ black sample11.py --preview  
All done! ✨ 🍰 ✨
1 file left unchanged.
```

```python:sample11.py
print(
    "あい　うえお"
)
```

フォーマット後の状態では、Ruffは`E501`エラーになりません。

## Ruffのv0.0.260未満を利用する
Ruffのv0.0.260未満では、"line-width"の計算では全角文字を1文字としてカウントするので、blackの通常版と"line-width"の計算方法が同じになります。


## Ruffでルール`E501`また`RU100`をチェックしない
解決策というか回避策です。


## "3.blackでフォーマット"後のときに、`# noqa: E501,RUF100`を付与する


```python
print(
    "あい　うえお"  # noqa: E501,RUF100
)
```