Skip to content

Non-idempotent comment movement for ((x) # comment here ...) with parens and 3+ lines #16151

@huonw

Description

@huonw

Description

Context

We have some code along the lines of:

result = (
   (await query_the_thing(mypy_doesnt_understand))  # type: ignore[x]
   .foo()
   .bar()
)

Switching from black to ruff format results in the comment moving:

  • in a non-idempotent way, requiring two ruff format invocations
  • to a 'strange' location

Reproducer

Formatting this code requires multiple invocations of ruff format to be "stable"/reach a fixed point:

  1. Input:

    (
        (x)  # a
        .y()
        .z()
    )
  2. Output of formatting 1 (change: comment is shifted to next line):

    (
        (x)
          # a
        .y()
        .z()
    )
  3. Output of formatting 2 (change: comment is unindented):

    (
        (x)
        # a
        .y()
        .z()
    )

Playground links:

Expected behaviour

ruff format should be idempotent: the output of formatting should not have any changes if formatted again.

That is, the first format call (step 1 -> 2) should give the final result with the comment in its final position (unindented), and the second (step 2 -> 3) should not change the code.

Alternatively, the formatting for this circumstance changed to sidestep the issue. It's seems weird to me that this comment is being wrapped. It moving the comment to the next line also problems if the comment is a # type: ... or # noqa pragma.

See also "Ablations".

Ablations

This stops reproducing with these variations of the Input (1):

  1. Only one extra line after the comment (no change: comment remains where it is)

    (
        (x)  # a
        .y()
    )
  2. No parens around x (becomes x.y().z() # a)

    (
        x  # a
        .y()
        .z()
    )
  3. Comment on a different line (no change):

    (
        (x)
        .y()  # a
        .z()
    )

Metadata

Ruff version: 0.9.6

Settings (default play.ruff.rs ones):

Details
{
  "preview": false,
  "builtins": [],
  "target-version": "py312",
  "line-length": 88,
  "indent-width": 4,
  "lint": {
    "allowed-confusables": [],
    "dummy-variable-rgx": "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$",
    "extend-select": [],
    "extend-fixable": [],
    "external": [],
    "ignore": [],
    "select": [
      "ALL"
    ]
  },
  "format": {
    "indent-style": "space",
    "quote-style": "double"
  }
}

Search keywords: idempotent, idempotency, reformat, repeated invocations, fixed point, stable


Thanks for ruff!

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingformatterRelated to the formattergreat writeupA wonderful example of a quality contribution

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions