How to force chained method calls to remain multiline using “ruff format”?

# Postmortem: Ruff Format Collapsing Multi-Line Chained Method Calls Against Developer Intent

## Summary
• Ruff formatter unexpectedly collapsed multiline chained method calls into a single line  
• Attempts to use `preview = true` and adjust `pyproject.toml` configuration didn't preserve desired formatting  
• Behavior occurred inconsistently based on trailing comments or argument length  

## Root Cause
• Ruff's formatter uses heuristic line-breaking rules lacking explicit "force multiline" markers  
• Configuration gaps exist between:  
  - Existing GitHub issue [#8049](https://github.com/astral-sh/ruff/issues/8049) tracking multiline chaining  
  - Actual implementation status (Preview feature partially shipped/unstable)  
• Ambiguous interaction between:  
  - Parentheses wrapping  
  - Embedded comments  
  - Method call length  

## Why This Happens in Real Systems
• Auto-formatters prioritize consistency over contextual readability needs  
• Complex chained calls fall into Ruff's "expression breaking" gray area where:  
  - Rule-based heuristics misjudge developer intent  
  - Lack of explicit syntax (like trailing `\`) forces guessing  
• Legacy code migration pain points:  
  - Existing stylistic patterns conflict with formatter defaults  
  - Gradual adoption of Preview features causes inconsistency  

## Real-World Impact
• **Readability degradation**: Critical visual grouping signals lost in long chains  
```python
# Collapsed version obscures workflow sequence
(greeter.greet("Anton").greet("Anabelle").greet("Aaron"))

Version control churn: Battles between formatter/developer causing repeated revert-format cycles
Onboarding friction: New engineers struggle with unexpected formatting changes
Tool distrust: Engineers bypass formatting for critical sections via # noqa

Example or Code

Original with working parentheses + comment:

(
  greeter  # Explicit comment preserves multiline
  .greet("Anton")
  .greet("Anabelle")
  .greet("Aaron")
)

Failing case with argument-length sensitivity:

# Formatter collapses first two chains
(
  main_menu
  .click_on_main_menu("short").click_on_main_menu_item("this_string_is_too_long")
)

How Senior Engineers Fix It

  1. Immediate workaround: Inject comments to force breaks
    (main_menu
      .method1()  # force-break
      .method2()
    )
  2. Strategic defense: Wrap chain segments in parentheses
    (method.one()).two().three()
  3. Toolchain adaptation: Combine Ruff with black via --format flag for compound chains
  4. Config freeze: Disable unstable preview features until behavior stabilizes
    [tool.ruff]
    preview = false  # Revert to stable formatting rules
  5. Team alignment: Standardize trailing # fmt: skip annotations for critical chains

Why Juniors Miss It

• Assume formatters handle all readability concerns “magically”
• Lack context about Ruff’s Preview feature lifecycle stage
• Unaware of heuristic limitations:

  • Comment placement sensitivity
  • Parentheses interaction pitfalls
    • Hesitate to disable formatting after investing in linting pipeline setup
    • Focus on functionality over formatting side-effects

    
    # Junior developer's expectation vs reality
    expectation: 
    (chain
    .line()
    .breaks()
    )

reality:
(chain.line().breaks()) # Why??