Files
ruff/crates/ruff_python_formatter/tests/snapshots/format@fluent.py.snap
Dylan 4e1cf5747a Fluent formatting of method chains (#21369)
This PR implements a modification (in preview) to fluent formatting for
method chains: We break _at_ the first call instead of _after_.

For example, we have the following diff between `main` and this PR (with
`line-length=8` so I don't have to stretch out the text):

```diff
 x = (
-    df.merge()
+    df
+    .merge()
     .groupby()
     .agg()
     .filter()
 )
```

## Explanation of current implementation

Recall that we traverse the AST to apply formatting. A method chain,
while read left-to-right, is stored in the AST "in reverse". So if we
start with something like

```python
a.b.c.d().e.f()
```

then the first syntax node we meet is essentially `.f()`. So we have to
peek ahead. And we actually _already_ do this in our current fluent
formatting logic: we peek ahead to count how many calls we have in the
chain to see whether we should be using fluent formatting or now.

In this implementation, we actually _record_ this number inside the enum
for `CallChainLayout`. That is, we make the variant `Fluent` hold an
`AttributeState`. This state can either be:

- The number of call-like attributes preceding the current attribute
- The state `FirstCallOrSubscript` which means we are at the first
call-like attribute in the chain (reading from left to right)
- The state `BeforeFirstCallOrSubscript` which means we are in the
"first group" of attributes, preceding that first call.

In our example, here's what it looks like at each attribute:

```
a.b.c.d().e.f @ Fluent(CallsOrSubscriptsPreceding(1))
a.b.c.d().e @ Fluent(CallsOrSubscriptsPreceding(1))
a.b.c.d @ Fluent(FirstCallOrSubscript)
a.b.c @ Fluent(BeforeFirstCallOrSubscript)
a.b @ Fluent(BeforeFirstCallOrSubscript)
```

Now, as we descend down from the parent expression, we pass along this
little piece of state and modify it as we go to track where we are. This
state doesn't do anything except when we are in `FirstCallOrSubscript`,
in which case we add a soft line break.

Closes #8598

---------

Co-authored-by: Brent Westbrook <36778786+ntBre@users.noreply.github.com>
2025-12-15 09:29:50 -06:00

164 lines
2.5 KiB
Plaintext

---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.py
---
## Input
```python
# Fixtures for fluent formatting of call chains
# Note that `fluent.options.json` sets line width to 8
x = a.b()
x = a.b().c()
x = a.b().c().d
x = a.b.c.d().e()
x = a.b.c().d.e().f.g()
# Consecutive calls/subscripts are grouped together
# for the purposes of fluent formatting (though, as 2025.12.15,
# there may be a break inside of one of these
# calls/subscripts, but that is unrelated to the fluent format.)
x = a()[0]().b().c()
x = a.b()[0].c.d()[1]().e
# Parentheses affect both where the root of the call
# chain is and how many calls we require before applying
# fluent formatting (just 1, in the presence of a parenthesized
# root, as of 2025.12.15.)
x = (a).b()
x = (a()).b()
x = (a.b()).d.e()
x = (a.b().d).e()
```
## Outputs
### Output 1
```
indent-style = space
line-width = 8
indent-width = 4
quote-style = Double
line-ending = LineFeed
magic-trailing-comma = Respect
docstring-code = Disabled
docstring-code-line-width = "dynamic"
preview = Disabled
target_version = 3.10
source_type = Python
```
```python
# Fixtures for fluent formatting of call chains
# Note that `fluent.options.json` sets line width to 8
x = a.b()
x = a.b().c()
x = (
a.b()
.c()
.d
)
x = a.b.c.d().e()
x = (
a.b.c()
.d.e()
.f.g()
)
# Consecutive calls/subscripts are grouped together
# for the purposes of fluent formatting (though, as 2025.12.15,
# there may be a break inside of one of these
# calls/subscripts, but that is unrelated to the fluent format.)
x = (
a()[
0
]()
.b()
.c()
)
x = (
a.b()[
0
]
.c.d()[
1
]()
.e
)
# Parentheses affect both where the root of the call
# chain is and how many calls we require before applying
# fluent formatting (just 1, in the presence of a parenthesized
# root, as of 2025.12.15.)
x = (
a
).b()
x = (
a()
).b()
x = (
a.b()
).d.e()
x = (
a.b().d
).e()
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -7,7 +7,8 @@
x = a.b().c()
x = (
- a.b()
+ a
+ .b()
.c()
.d
)
@@ -15,7 +16,8 @@
x = a.b.c.d().e()
x = (
- a.b.c()
+ a.b
+ .c()
.d.e()
.f.g()
)
@@ -34,7 +36,8 @@
)
x = (
- a.b()[
+ a
+ .b()[
0
]
.c.d()[
```