Skip to content

docformatter deadlock with ruff-format: blank lines around nested definitions #354

Description

@Borda

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 def with one-liner docstring: docformatter removes the blank line
    after it; ruff-format adds it back.
  • Loop B — nested class with one-liner docstring (blank = true in 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    freshThis is a new issue

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions