docformatter deadlock with ruff-format: blank lines around nested definitions
まだ誰も着手していません。
評価
調査の方向性
example.py と example_class.py にある最小限の再現例から始め、次に示されている設定で docformatter と ruff format を実行して、両方のループを再現します。ネストされた 1 行の docstring の後にある空行を docformatter がどのように分類するかを追跡し、2 つのケースの回帰テストを追加します。繰り返し実行しても収束し、どちらのツールももう一方の変更を元に戻さなければ完了です。
索引モデルが issue の本文から書いたものです。
説明
Summary
docformatter and ruff-format enter an infinite correction loop in pre-commit when
test functions contain nested def or class definitions whose only body is a one-line
docstring. The tools disagree on whether a blank line should follow the nested definition,
so each tool's output is the other tool's input and pre-commit never converges.
Two distinct sub-issues are present:
- Loop A — nested
defwith one-liner docstring: docformatter removes the blank line
after it; ruff-format adds it back. - Loop B — nested
classwith one-liner docstring (blank = truein config): docformatter
adds an extra blank line; ruff-format removes it.
Versions
| Tool | Version |
|---|---|
| docformatter (pip / pre-commit hook) | 1.7.8 |
ruff-format (pre-commit hook astral-sh/ruff-pre-commit) |
v0.15.9 |
| Python | 3.10.11 |
Configuration
pyproject.toml:
[tool.docformatter]
recursive = true
wrap-summaries = 120
wrap-descriptions = 120
blank = true
.pre-commit-config.yaml (relevant hooks):
- repo: https://github.com/PyCQA/docformatter
rev: v1.7.8
hooks:
- id: docformatter
language_version: python3.10
additional_dependencies: [tomli]
args: ["--in-place"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.9
hooks:
- id: ruff-check
args: ["--fix"]
- id: ruff-format
Minimal reproduction — Loop A
Create example.py:
def outer() -> None:
"""Outer function."""
def inner() -> None:
"""One-liner docstring."""
next_statement = 1
Run docformatter:
docformatter --in-place --blank example.py
Result — blank line removed:
def outer() -> None:
"""Outer function."""
def inner() -> None:
"""One-liner docstring."""
next_statement = 1 # ← blank line gone
Run ruff-format on that output:
ruff format example.py
Result — blank line restored (back to original):
def outer() -> None:
"""Outer function."""
def inner() -> None:
"""One-liner docstring."""
next_statement = 1 # ← blank line back
Running both again repeats the cycle indefinitely.
Minimal reproduction — Loop B (blank = true)
Create example_class.py:
def outer() -> None:
"""Outer function."""
class Inner:
"""One-liner class docstring."""
@some_decorator
class Another:
"""Another class."""
With blank = true, docformatter inserts an extra blank line after the one-liner
class docstring (before @some_decorator), producing two consecutive blank lines.
ruff-format then removes the extra one. Each tool undoes the other.
Root cause
Loop A
docformatter interprets the blank line between the closing """ of inner()'s docstring
and next_statement as being inside inner()'s function body and removes it as a
PEP 257 D202 violation ("no blank lines allowed after function docstring").
The blank line is not inside inner() — it is in the outer scope, separating two
statements. docformatter misattributes it because the last token of inner()'s body is
the closing """ of a one-liner docstring on the same line as the opening """, with no
other body statements.
ruff-format (Black-compatible) correctly requires the blank line between the nested
definition and the following statement per E301 / PEP 8.
Loop B
blank = true causes docformatter to insert a blank line at the end of one-liner class
docstrings when they are followed by another definition. Combined with the blank line
already present, this produces two blank lines (E303), which ruff-format then reduces
back to one.
Expected behaviour
docformatter should not remove the blank line that follows a nested function definition
whose only body is a one-liner docstring. That blank line belongs to the enclosing
scope, not to the nested function.
Workaround
Remove blank = true from [tool.docformatter] to mitigate Loop B.
Loop A has no configuration-level workaround; the only option is to exclude the affected
files from docformatter or avoid the nested-def-with-one-liner-docstring pattern in test
code.
- 主要言語
- Python
- スター
- 598
- フォーク
- 93
- 平均マージ
- 12日 10時間
- マージ済み PR(30日)
- 1
環境構築
このプロジェクトの環境構築ファイルはまだ確認していません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
PyCQA/docformatter のほかの issue
-
fresh
難易度 3/5 1〜2日 初心者へのやさしさ 78/100
PyCQA/docformatter#385 ·
-
C: convention P: bug U: high
難易度 3/5 1〜2日 初心者へのやさしさ 74/100
PyCQA/docformatter#367 · コメント 1 件 ·
-
1.7.8 rewrites the contents of a non-docstring triple-quoted string, silently changing its valueオープンC: convention P: bug U: high
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
PyCQA/docformatter#366 ·
-
C: stakeholder P: enhancement U: low
難易度 3/5 1〜2日 初心者へのやさしさ 65/100
PyCQA/docformatter#346 ·
-
C: convention P: bug U: high
難易度 3/5 1〜2日 初心者へのやさしさ 48/100
PyCQA/docformatter#345 · リアクション 1 件 ·
PyCQA/docformatter の issue をすべて見る
似ている issue
-
難易度 1/5 1時間未満 初心者へのやさしさ 72/100
letsencrypt/cp-cps#353 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
PedestrianDynamics/pyFDS-Evac#394 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
DOI-USGS/pywatershed#421 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
python-pillow/Pillow#10087 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信