mirror of
https://github.com/astral-sh/ruff
synced 2026-01-09 07:34:06 -05:00
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>
This commit is contained in:
@@ -501,6 +501,48 @@ If you want Ruff to split an f-string across multiple lines, ensure there's a li
|
||||
[self-documenting f-string]: https://realpython.com/python-f-strings/#self-documenting-expressions-for-debugging
|
||||
[configured quote style]: settings.md/#format_quote-style
|
||||
|
||||
#### Fluent layout for method chains
|
||||
|
||||
At times, when developers write long chains of methods on an object, such as
|
||||
|
||||
```python
|
||||
x = df.filter(cond).agg(func).merge(other)
|
||||
```
|
||||
|
||||
the intent is to perform a sequence of transformations or operations
|
||||
on a fixed object of interest - in this example, the object `df`.
|
||||
Assuming the assigned expression exceeds the `line-length`, this preview
|
||||
style will format the above as:
|
||||
|
||||
```python
|
||||
x = (
|
||||
df
|
||||
.filter(cond)
|
||||
.agg(func)
|
||||
.merge(other)
|
||||
)
|
||||
```
|
||||
|
||||
This deviates from the stable formatting, and also from Black, both
|
||||
of which would produce:
|
||||
|
||||
|
||||
```python
|
||||
x = (
|
||||
df.filter(cond)
|
||||
.agg(func)
|
||||
.merge(other)
|
||||
)
|
||||
```
|
||||
|
||||
Both the stable and preview formatting are variants of something
|
||||
called a **fluent layout**.
|
||||
|
||||
In general, this preview style differs from the stable style
|
||||
only at the first attribute that precedes
|
||||
a call or subscript. The preview formatting breaks _before_ this attribute,
|
||||
while the stable formatting breaks _after_ the call or subscript.
|
||||
|
||||
## Sorting imports
|
||||
|
||||
Currently, the Ruff formatter does not sort imports. In order to both sort imports and format,
|
||||
|
||||
Reference in New Issue
Block a user