mirror of https://github.com/astral-sh/ruff
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:
parent
cbfecfaf41
commit
4e1cf5747a
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.options.json
vendored
Normal file
1
crates/ruff_python_formatter/resources/test/fixtures/ruff/fluent.options.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[{"line_width":8}]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# 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()
|
||||
|
|
@ -216,3 +216,69 @@ max_message_id = (
|
|||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
].values().attr
|
||||
)
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (entry_count := len(names := cursor.execute(
|
||||
'SELECT name FROM address_book WHERE address=?',
|
||||
(address,),
|
||||
).fetchall().some_attr)) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (aaaaaaaaaaaaaaaaaaaaaa).bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc().dddddddddddddddddddddddd().eeeeeeeeeeee
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use crate::expression::parentheses::{
|
|||
NeedsParentheses, OptionalParentheses, Parentheses, is_expression_parenthesized,
|
||||
};
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_fluent_layout_split_first_call_enabled;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprAttribute {
|
||||
|
|
@ -47,20 +48,26 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
)
|
||||
};
|
||||
|
||||
if call_chain_layout == CallChainLayout::Fluent {
|
||||
if call_chain_layout.is_fluent() {
|
||||
if parenthesize_value {
|
||||
// Don't propagate the call chain layout.
|
||||
value.format().with_options(Parentheses::Always).fmt(f)?;
|
||||
} else {
|
||||
match value.as_ref() {
|
||||
Expr::Attribute(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
Expr::Call(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
Expr::Subscript(expr) => {
|
||||
expr.format().with_options(call_chain_layout).fmt(f)?;
|
||||
expr.format()
|
||||
.with_options(call_chain_layout.transition_after_attribute())
|
||||
.fmt(f)?;
|
||||
}
|
||||
_ => {
|
||||
value.format().with_options(Parentheses::Never).fmt(f)?;
|
||||
|
|
@ -105,8 +112,30 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
// Allow the `.` on its own line if this is a fluent call chain
|
||||
// and the value either requires parenthesizing or is a call or subscript expression
|
||||
// (it's a fluent chain but not the first element).
|
||||
else if call_chain_layout == CallChainLayout::Fluent {
|
||||
if parenthesize_value || value.is_call_expr() || value.is_subscript_expr() {
|
||||
//
|
||||
// In preview we also break _at_ the first call in the chain.
|
||||
// For example:
|
||||
//
|
||||
// ```diff
|
||||
// # stable formatting vs. preview
|
||||
// x = (
|
||||
// - df.merge()
|
||||
// + df
|
||||
// + .merge()
|
||||
// .groupby()
|
||||
// .agg()
|
||||
// .filter()
|
||||
// )
|
||||
// ```
|
||||
else if call_chain_layout.is_fluent() {
|
||||
if parenthesize_value
|
||||
|| value.is_call_expr()
|
||||
|| value.is_subscript_expr()
|
||||
// Remember to update the doc-comment above when
|
||||
// stabilizing this behavior.
|
||||
|| (is_fluent_layout_split_first_call_enabled(f.context())
|
||||
&& call_chain_layout.is_first_call_like())
|
||||
{
|
||||
soft_line_break().fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,8 +177,8 @@ impl FormatNodeRule<ExprAttribute> for FormatExprAttribute {
|
|||
)
|
||||
});
|
||||
|
||||
let is_call_chain_root = self.call_chain_layout == CallChainLayout::Default
|
||||
&& call_chain_layout == CallChainLayout::Fluent;
|
||||
let is_call_chain_root =
|
||||
self.call_chain_layout == CallChainLayout::Default && call_chain_layout.is_fluent();
|
||||
if is_call_chain_root {
|
||||
write!(f, [group(&format_inner)])
|
||||
} else {
|
||||
|
|
@ -169,7 +198,8 @@ impl NeedsParentheses for ExprAttribute {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if context.comments().has_dangling(self) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
|
|||
func.format().with_options(Parentheses::Always).fmt(f)
|
||||
} else {
|
||||
match func.as_ref() {
|
||||
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Attribute(expr) => expr
|
||||
.format()
|
||||
.with_options(call_chain_layout.decrement_call_like_count())
|
||||
.fmt(f),
|
||||
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
_ => func.format().with_options(Parentheses::Never).fmt(f),
|
||||
|
|
@ -67,9 +70,7 @@ impl FormatNodeRule<ExprCall> for FormatExprCall {
|
|||
// queryset.distinct().order_by(field.name).values_list(field_name_flat_long_long=True)
|
||||
// )
|
||||
// ```
|
||||
if call_chain_layout == CallChainLayout::Fluent
|
||||
&& self.call_chain_layout == CallChainLayout::Default
|
||||
{
|
||||
if call_chain_layout.is_fluent() && self.call_chain_layout == CallChainLayout::Default {
|
||||
group(&fmt_func).fmt(f)
|
||||
} else {
|
||||
fmt_func.fmt(f)
|
||||
|
|
@ -87,7 +88,8 @@ impl NeedsParentheses for ExprCall {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if context.comments().has_dangling(self) {
|
||||
|
|
|
|||
|
|
@ -397,7 +397,8 @@ impl Format<PyFormatContext<'_>> for FormatBody<'_> {
|
|||
body.into(),
|
||||
comments.ranges(),
|
||||
f.context().source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
parenthesize_if_expands(&unparenthesized).fmt(f)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
|
|||
value.format().with_options(Parentheses::Always).fmt(f)
|
||||
} else {
|
||||
match value.as_ref() {
|
||||
Expr::Attribute(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Attribute(expr) => expr
|
||||
.format()
|
||||
.with_options(call_chain_layout.decrement_call_like_count())
|
||||
.fmt(f),
|
||||
Expr::Call(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
Expr::Subscript(expr) => expr.format().with_options(call_chain_layout).fmt(f),
|
||||
_ => value.format().with_options(Parentheses::Never).fmt(f),
|
||||
|
|
@ -71,8 +74,8 @@ impl FormatNodeRule<ExprSubscript> for FormatExprSubscript {
|
|||
.fmt(f)
|
||||
});
|
||||
|
||||
let is_call_chain_root = self.call_chain_layout == CallChainLayout::Default
|
||||
&& call_chain_layout == CallChainLayout::Fluent;
|
||||
let is_call_chain_root =
|
||||
self.call_chain_layout == CallChainLayout::Default && call_chain_layout.is_fluent();
|
||||
if is_call_chain_root {
|
||||
write!(f, [group(&format_inner)])
|
||||
} else {
|
||||
|
|
@ -92,7 +95,8 @@ impl NeedsParentheses for ExprSubscript {
|
|||
self.into(),
|
||||
context.comments().ranges(),
|
||||
context.source(),
|
||||
) == CallChainLayout::Fluent
|
||||
)
|
||||
.is_fluent()
|
||||
{
|
||||
OptionalParentheses::Multiline
|
||||
} else if is_expression_parenthesized(
|
||||
|
|
|
|||
|
|
@ -876,6 +876,22 @@ impl<'a> First<'a> {
|
|||
/// )
|
||||
/// ).all()
|
||||
/// ```
|
||||
///
|
||||
/// In [`preview`](crate::preview::is_fluent_layout_split_first_call_enabled), we also track the position of the leftmost call or
|
||||
/// subscript on an attribute in the chain and break just before the dot.
|
||||
///
|
||||
/// So, for example, the right-hand summand in the above expression
|
||||
/// would get formatted as:
|
||||
/// ```python
|
||||
/// Blog.objects
|
||||
/// .filter(
|
||||
/// entry__headline__contains="McCartney",
|
||||
/// )
|
||||
/// .limit_results[:10]
|
||||
/// .filter(
|
||||
/// entry__pub_date__year=2010,
|
||||
/// )
|
||||
/// ```
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum CallChainLayout {
|
||||
/// The root of a call chain
|
||||
|
|
@ -883,19 +899,149 @@ pub enum CallChainLayout {
|
|||
Default,
|
||||
|
||||
/// A nested call chain element that uses fluent style.
|
||||
Fluent,
|
||||
Fluent(AttributeState),
|
||||
|
||||
/// A nested call chain element not using fluent style.
|
||||
NonFluent,
|
||||
}
|
||||
|
||||
/// Records information about the current position within
|
||||
/// a call chain.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AttributeState {
|
||||
/// Stores the number of calls or subscripts
|
||||
/// to the left of the current position in a chain.
|
||||
///
|
||||
/// Consecutive calls/subscripts on a single
|
||||
/// object only count once. For example, if we are at
|
||||
/// `c` in `a.b()[0]()().c()` then this number would be 1.
|
||||
///
|
||||
/// Caveat: If the root of the chain is parenthesized,
|
||||
/// it contributes +1 to this count, even if it is not
|
||||
/// a call or subscript. But the name
|
||||
/// `CallLikeOrParenthesizedRootPreceding`
|
||||
/// is a tad unwieldy, and this also rarely occurs.
|
||||
CallLikePreceding(u32),
|
||||
/// Indicates that we are at the first called or
|
||||
/// subscripted object in the chain
|
||||
///
|
||||
/// For example, if we are at `b` in `a.b()[0]()().c()`
|
||||
FirstCallLike,
|
||||
/// Indicates that we are to the left of the first
|
||||
/// called or subscripted object in the chain, and therefore
|
||||
/// need not break.
|
||||
///
|
||||
/// For example, if we are at `a` in `a.b()[0]()().c()`
|
||||
BeforeFirstCallLike,
|
||||
}
|
||||
|
||||
impl CallChainLayout {
|
||||
/// Returns new state decreasing count of remaining calls/subscripts
|
||||
/// to traverse, or the state `FirstCallOrSubscript`, as appropriate.
|
||||
#[must_use]
|
||||
pub(crate) fn decrement_call_like_count(self) -> Self {
|
||||
match self {
|
||||
Self::Fluent(AttributeState::CallLikePreceding(x)) => {
|
||||
if x > 1 {
|
||||
// Recall that we traverse call chains from right to
|
||||
// left. So after moving from a call/subscript into
|
||||
// an attribute, we _decrease_ the count of
|
||||
// _remaining_ calls or subscripts to the left of our
|
||||
// current position.
|
||||
Self::Fluent(AttributeState::CallLikePreceding(x - 1))
|
||||
} else {
|
||||
Self::Fluent(AttributeState::FirstCallLike)
|
||||
}
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns with state change
|
||||
/// `FirstCallOrSubscript` -> `BeforeFirstCallOrSubscript`
|
||||
/// and otherwise returns unchanged.
|
||||
#[must_use]
|
||||
pub(crate) fn transition_after_attribute(self) -> Self {
|
||||
match self {
|
||||
Self::Fluent(AttributeState::FirstCallLike) => {
|
||||
Self::Fluent(AttributeState::BeforeFirstCallLike)
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_first_call_like(self) -> bool {
|
||||
matches!(self, Self::Fluent(AttributeState::FirstCallLike))
|
||||
}
|
||||
|
||||
/// Returns either `Fluent` or `NonFluent` depending on a
|
||||
/// heuristic computed for the whole chain.
|
||||
///
|
||||
/// Explicitly, the criterion to return `Fluent` is
|
||||
/// as follows:
|
||||
///
|
||||
/// 1. Beginning from the right (i.e. the `expr` itself),
|
||||
/// traverse inwards past calls, subscripts, and attribute
|
||||
/// expressions until we meet the first expression that is
|
||||
/// either none of these or else is parenthesized. This will
|
||||
/// be the _root_ of the call chain.
|
||||
/// 2. Count the number of _attribute values_ that are _called
|
||||
/// or subscripted_ in the chain (note that this includes the
|
||||
/// root but excludes the rightmost attribute in the chain since
|
||||
/// it is not the _value_ of some attribute).
|
||||
/// 3. If the root is parenthesized, add 1 to that value.
|
||||
/// 4. If the total is at least 2, return `Fluent`. Otherwise
|
||||
/// return `NonFluent`
|
||||
pub(crate) fn from_expression(
|
||||
mut expr: ExprRef,
|
||||
comment_ranges: &CommentRanges,
|
||||
source: &str,
|
||||
) -> Self {
|
||||
let mut attributes_after_parentheses = 0;
|
||||
// TODO(dylan): Once the fluent layout preview style is
|
||||
// stabilized, see if it is possible to simplify some of
|
||||
// the logic around parenthesized roots. (While supporting
|
||||
// both styles it is more difficult to do this.)
|
||||
|
||||
// Count of attribute _values_ which are called or
|
||||
// subscripted, after the leftmost parenthesized
|
||||
// value.
|
||||
//
|
||||
// Examples:
|
||||
// ```
|
||||
// # Count of 3 - notice that .d()
|
||||
// # does not contribute
|
||||
// a().b().c[0]()().d()
|
||||
// # Count of 2 - notice that a()
|
||||
// # does not contribute
|
||||
// (a()).b().c[0].d
|
||||
// ```
|
||||
let mut computed_attribute_values_after_parentheses = 0;
|
||||
|
||||
// Similar to the above, but instead looks at all calls
|
||||
// and subscripts rather than looking only at those on
|
||||
// _attribute values_. So this count can differ from the
|
||||
// above.
|
||||
//
|
||||
// Examples of `computed_attribute_values_after_parentheses` vs
|
||||
// `call_like_count`:
|
||||
//
|
||||
// a().b ---> 1 vs 1
|
||||
// a.b().c --> 1 vs 1
|
||||
// a.b() ---> 0 vs 1
|
||||
let mut call_like_count = 0;
|
||||
|
||||
// Going from right to left, we traverse calls, subscripts,
|
||||
// and attributes until we get to an expression of a different
|
||||
// kind _or_ to a parenthesized expression. This records
|
||||
// the case where we end the traversal at a parenthesized expression.
|
||||
//
|
||||
// In these cases, the inferred semantics of the chain are different.
|
||||
// We interpret this as the user indicating:
|
||||
// "this parenthesized value is the object of interest and we are
|
||||
// doing transformations on it". This increases our confidence that
|
||||
// this should be fluently formatted, and also means we should make
|
||||
// our first break after this value.
|
||||
let mut root_value_parenthesized = false;
|
||||
loop {
|
||||
match expr {
|
||||
ExprRef::Attribute(ast::ExprAttribute { value, .. }) => {
|
||||
|
|
@ -907,10 +1053,10 @@ impl CallChainLayout {
|
|||
// ```
|
||||
if is_expression_parenthesized(value.into(), comment_ranges, source) {
|
||||
// `(a).b`. We preserve these parentheses so don't recurse
|
||||
attributes_after_parentheses += 1;
|
||||
root_value_parenthesized = true;
|
||||
break;
|
||||
} else if matches!(value.as_ref(), Expr::Call(_) | Expr::Subscript(_)) {
|
||||
attributes_after_parentheses += 1;
|
||||
computed_attribute_values_after_parentheses += 1;
|
||||
}
|
||||
|
||||
expr = ExprRef::from(value.as_ref());
|
||||
|
|
@ -925,31 +1071,68 @@ impl CallChainLayout {
|
|||
// ```
|
||||
ExprRef::Call(ast::ExprCall { func: inner, .. })
|
||||
| ExprRef::Subscript(ast::ExprSubscript { value: inner, .. }) => {
|
||||
// We preserve these parentheses so don't recurse
|
||||
// e.g. (a)[0].x().y().z()
|
||||
// ^stop here
|
||||
if is_expression_parenthesized(inner.into(), comment_ranges, source) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Accumulate the `call_like_count`, but we only
|
||||
// want to count things like `a()[0]()()` once.
|
||||
if !inner.is_call_expr() && !inner.is_subscript_expr() {
|
||||
call_like_count += 1;
|
||||
}
|
||||
|
||||
expr = ExprRef::from(inner.as_ref());
|
||||
}
|
||||
_ => {
|
||||
// We to format the following in fluent style:
|
||||
// ```
|
||||
// f2 = (a).w().t(1,)
|
||||
// ^ expr
|
||||
// ```
|
||||
if is_expression_parenthesized(expr, comment_ranges, source) {
|
||||
attributes_after_parentheses += 1;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// We preserve these parentheses so don't recurse
|
||||
if is_expression_parenthesized(expr, comment_ranges, source) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if attributes_after_parentheses < 2 {
|
||||
|
||||
if computed_attribute_values_after_parentheses + u32::from(root_value_parenthesized) < 2 {
|
||||
CallChainLayout::NonFluent
|
||||
} else {
|
||||
CallChainLayout::Fluent
|
||||
CallChainLayout::Fluent(AttributeState::CallLikePreceding(
|
||||
// We count a parenthesized root value as an extra
|
||||
// call for the purposes of tracking state.
|
||||
//
|
||||
// The reason is that, in this case, we want the first
|
||||
// "special" break to happen right after the root, as
|
||||
// opposed to right after the first called/subscripted
|
||||
// attribute.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// ```
|
||||
// (object_of_interest)
|
||||
// .data.filter()
|
||||
// .agg()
|
||||
// .etc()
|
||||
// ```
|
||||
//
|
||||
// instead of (in preview):
|
||||
//
|
||||
// ```
|
||||
// (object_of_interest)
|
||||
// .data
|
||||
// .filter()
|
||||
// .etc()
|
||||
// ```
|
||||
//
|
||||
// For comparison, if we didn't have parentheses around
|
||||
// the root, we want (and get, in preview):
|
||||
//
|
||||
// ```
|
||||
// object_of_interest.data
|
||||
// .filter()
|
||||
// .agg()
|
||||
// .etc()
|
||||
// ```
|
||||
call_like_count + u32::from(root_value_parenthesized),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -972,9 +1155,13 @@ impl CallChainLayout {
|
|||
CallChainLayout::NonFluent
|
||||
}
|
||||
}
|
||||
layout @ (CallChainLayout::Fluent | CallChainLayout::NonFluent) => layout,
|
||||
layout @ (CallChainLayout::Fluent(_) | CallChainLayout::NonFluent) => layout,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_fluent(self) -> bool {
|
||||
matches!(self, CallChainLayout::Fluent(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -59,3 +59,10 @@ pub(crate) const fn is_avoid_parens_for_long_as_captures_enabled(
|
|||
pub(crate) const fn is_parenthesize_lambda_bodies_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// Returns `true` if the
|
||||
/// [`fluent_layout_split_first_call`](https://github.com/astral-sh/ruff/pull/21369) preview
|
||||
/// style is enabled.
|
||||
pub(crate) const fn is_fluent_layout_split_first_call_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ class Random:
|
|||
}
|
||||
x = {
|
||||
"foobar": (123) + 456,
|
||||
@@ -97,24 +94,20 @@
|
||||
@@ -97,24 +94,21 @@
|
||||
|
||||
|
||||
my_dict = {
|
||||
|
|
@ -221,13 +221,14 @@ class Random:
|
|||
- .second_call()
|
||||
- .third_call(some_args="some value")
|
||||
- )
|
||||
+ "a key in my dict": MyClass.some_attribute.first_call()
|
||||
+ "a key in my dict": MyClass.some_attribute
|
||||
+ .first_call()
|
||||
+ .second_call()
|
||||
+ .third_call(some_args="some value")
|
||||
}
|
||||
|
||||
{
|
||||
@@ -139,17 +132,17 @@
|
||||
@@ -139,17 +133,17 @@
|
||||
|
||||
class Random:
|
||||
def func():
|
||||
|
|
@ -363,7 +364,8 @@ my_dict = {
|
|||
/ 100000.0
|
||||
}
|
||||
my_dict = {
|
||||
"a key in my dict": MyClass.some_attribute.first_call()
|
||||
"a key in my dict": MyClass.some_attribute
|
||||
.first_call()
|
||||
.second_call()
|
||||
.third_call(some_args="some value")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/await.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -142,3 +141,20 @@ test_data = await (
|
|||
.to_list()
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -65,7 +65,8 @@
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/8644
|
||||
test_data = await (
|
||||
- Stream.from_async(async_data)
|
||||
+ Stream
|
||||
+ .from_async(async_data)
|
||||
.flat_map_async()
|
||||
.map()
|
||||
.filter_async(is_valid_data)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/call.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -557,3 +556,20 @@ result = (
|
|||
|
||||
result = (object[complicate_caller])("argument").a["b"].test(argument)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -57,7 +57,8 @@
|
||||
|
||||
# Call chains/fluent interface (https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#call-chains)
|
||||
result = (
|
||||
- session.query(models.Customer.id)
|
||||
+ session
|
||||
+ .query(models.Customer.id)
|
||||
.filter(
|
||||
models.Customer.account_id == 10000,
|
||||
models.Customer.email == "user@example.org",
|
||||
```
|
||||
|
|
|
|||
|
|
@ -2155,7 +2155,7 @@ transform = (
|
|||
),
|
||||
param(
|
||||
lambda left, right: (
|
||||
@@ -471,9 +463,9 @@
|
||||
@@ -471,15 +463,16 @@
|
||||
),
|
||||
param(lambda left, right: ibis.timestamp("2017-04-01").cast(dt.date)),
|
||||
param(
|
||||
|
|
@ -2168,7 +2168,15 @@ transform = (
|
|||
),
|
||||
# This is too long on one line in the lambda body and gets wrapped
|
||||
# inside the body.
|
||||
@@ -507,16 +499,18 @@
|
||||
param(
|
||||
lambda left, right: (
|
||||
- ibis.timestamp("2017-04-01")
|
||||
+ ibis
|
||||
+ .timestamp("2017-04-01")
|
||||
.cast(dt.date)
|
||||
.between(left, right)
|
||||
.between(left, right)
|
||||
@@ -507,16 +500,18 @@
|
||||
]
|
||||
|
||||
# adds parentheses around the body
|
||||
|
|
@ -2190,7 +2198,7 @@ transform = (
|
|||
|
||||
lambda x, y, z: (
|
||||
x + y + z
|
||||
@@ -527,7 +521,7 @@
|
||||
@@ -527,7 +522,7 @@
|
||||
x + y + z # trailing eol body
|
||||
)
|
||||
|
||||
|
|
@ -2199,7 +2207,7 @@ transform = (
|
|||
|
||||
lambda x, y, z: (
|
||||
# leading body
|
||||
@@ -539,21 +533,23 @@
|
||||
@@ -539,21 +534,23 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2233,7 +2241,7 @@ transform = (
|
|||
# dangling header comment
|
||||
source_bucket
|
||||
if name == source_bucket_name
|
||||
@@ -561,8 +557,7 @@
|
||||
@@ -561,8 +558,7 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2243,7 +2251,7 @@ transform = (
|
|||
source_bucket
|
||||
if name == source_bucket_name
|
||||
else storage.Bucket(mock_service, destination_bucket_name)
|
||||
@@ -570,61 +565,70 @@
|
||||
@@ -570,61 +566,71 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2293,7 +2301,8 @@ transform = (
|
|||
- .cast(dt.date)
|
||||
- .between(left, right)
|
||||
+ lambda left, right: (
|
||||
+ ibis.timestamp("2017-04-01") # comment
|
||||
+ ibis
|
||||
+ .timestamp("2017-04-01") # comment
|
||||
+ .cast(dt.date)
|
||||
+ .between(left, right)
|
||||
+ )
|
||||
|
|
@ -2346,7 +2355,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -637,27 +641,31 @@
|
||||
@@ -637,27 +643,31 @@
|
||||
(
|
||||
lambda
|
||||
# comment
|
||||
|
|
@ -2386,7 +2395,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -672,25 +680,28 @@
|
||||
@@ -672,25 +682,28 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
@ -2427,7 +2436,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -698,9 +709,9 @@
|
||||
@@ -698,9 +711,9 @@
|
||||
# comment 1
|
||||
*ergs,
|
||||
# comment 2
|
||||
|
|
@ -2440,7 +2449,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -708,19 +719,20 @@
|
||||
@@ -708,19 +721,20 @@
|
||||
# 2
|
||||
left, # 3
|
||||
# 4
|
||||
|
|
@ -2471,7 +2480,7 @@ transform = (
|
|||
)
|
||||
)
|
||||
)
|
||||
@@ -738,48 +750,52 @@
|
||||
@@ -738,48 +752,52 @@
|
||||
foo(
|
||||
lambda from_ts, # but still wrap the body if it gets too long
|
||||
to_ts,
|
||||
|
|
@ -2548,7 +2557,7 @@ transform = (
|
|||
)
|
||||
|
||||
(
|
||||
@@ -828,8 +844,7 @@
|
||||
@@ -828,8 +846,7 @@
|
||||
)
|
||||
|
||||
(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/split_empty_brackets.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
---
|
||||
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()[
|
||||
```
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
|
@ -223,6 +222,72 @@ max_message_id = (
|
|||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
].values().attr
|
||||
)
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (entry_count := len(names := cursor.execute(
|
||||
'SELECT name FROM address_book WHERE address=?',
|
||||
(address,),
|
||||
).fetchall().some_attr)) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (aaaaaaaaaaaaaaaaaaaaaa).bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc().dddddddddddddddddddddddd().eeeeeeeeeeee
|
||||
```
|
||||
|
||||
## Output
|
||||
|
|
@ -466,4 +531,237 @@ max_message_id = (
|
|||
.sum()
|
||||
.baz()
|
||||
)
|
||||
|
||||
# Note in preview we split at `pl` which some
|
||||
# folks may dislike. (Similarly with common
|
||||
# `np` and `pd` invocations).
|
||||
#
|
||||
# This is because we cannot reliably predict,
|
||||
# just from syntax, whether a short identifier
|
||||
# is being used as a 'namespace' or as an 'object'.
|
||||
#
|
||||
# As of 2025.12.15, we do not indent methods in
|
||||
# fluent formatting. If we ever decide to do so,
|
||||
# it may make sense to special case call chain roots
|
||||
# that are shorter than the indent-width (like Prettier does).
|
||||
# This would have the benefit of handling these common
|
||||
# two-letter aliases for libraries.
|
||||
|
||||
|
||||
expr = (
|
||||
pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
.filter(
|
||||
[
|
||||
pl.col("path").str.contains(
|
||||
r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
),
|
||||
~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
]
|
||||
)
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
ext=pl.col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
.str.replace("rs", "Rust", literal=True)
|
||||
.str.replace("go", "Go", literal=True)
|
||||
.str.replace("asm", "Assembly", literal=True)
|
||||
.replace({"": None}),
|
||||
)
|
||||
.group_by(["month", "ext"])
|
||||
.agg(project_count=pl.col("project_name").n_unique())
|
||||
.drop_nulls(["ext"])
|
||||
.sort(["month", "project_count"], descending=True)
|
||||
)
|
||||
|
||||
|
||||
def indentation_matching_for_loop_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
for current_hidden_layer_sizes in self.component_config[
|
||||
HIDDEN_LAYERS_SIZES
|
||||
]
|
||||
.values()
|
||||
.attr
|
||||
)
|
||||
|
||||
|
||||
def indentation_matching_walrus_in_preview():
|
||||
if make_this:
|
||||
if more_nested_because_line_length:
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (
|
||||
entry_count := len(
|
||||
names := cursor.execute(
|
||||
"SELECT name FROM address_book WHERE address=?",
|
||||
(address,),
|
||||
)
|
||||
.fetchall()
|
||||
.some_attr
|
||||
)
|
||||
) == 0 or len(set(names)) > 1:
|
||||
return
|
||||
|
||||
|
||||
# behavior with parenthesized roots
|
||||
x = (
|
||||
(aaaaaaaaaaaaaaaaaaaaaa)
|
||||
.bbbbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc()
|
||||
.dddddddddddddddddddddddd()
|
||||
.eeeeeeeeeeee
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -21,7 +21,8 @@
|
||||
)
|
||||
|
||||
raise OsError("") from (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.filter(
|
||||
@@ -33,7 +34,8 @@
|
||||
)
|
||||
|
||||
raise OsError("sökdjffffsldkfjlhsakfjhalsökafhsöfdahsödfjösaaksjdllllllllllllll") from (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.filter(
|
||||
@@ -46,7 +48,8 @@
|
||||
|
||||
# Break only after calls and indexing
|
||||
b1 = (
|
||||
- session.query(models.Customer.id)
|
||||
+ session
|
||||
+ .query(models.Customer.id)
|
||||
.filter(
|
||||
models.Customer.account_id == account_id, models.Customer.email == email_address
|
||||
)
|
||||
@@ -54,7 +57,8 @@
|
||||
)
|
||||
|
||||
b2 = (
|
||||
- Blog.objects.filter(
|
||||
+ Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="Lennon",
|
||||
)
|
||||
.limit_results[:10]
|
||||
@@ -70,7 +74,8 @@
|
||||
).filter(
|
||||
entry__pub_date__year=2008,
|
||||
)
|
||||
- + Blog.objects.filter(
|
||||
+ + Blog.objects
|
||||
+ .filter(
|
||||
entry__headline__contains="McCartney",
|
||||
)
|
||||
.limit_results[:10]
|
||||
@@ -89,7 +94,8 @@
|
||||
d11 = x.e().e().e() #
|
||||
d12 = x.e().e().e() #
|
||||
d13 = (
|
||||
- x.e() #
|
||||
+ x
|
||||
+ .e() #
|
||||
.e()
|
||||
.e()
|
||||
)
|
||||
@@ -101,7 +107,8 @@
|
||||
|
||||
# Doesn't fit, fluent style
|
||||
d3 = (
|
||||
- x.e() #
|
||||
+ x
|
||||
+ .e() #
|
||||
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
|
||||
.esadjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk()
|
||||
)
|
||||
@@ -218,7 +225,8 @@
|
||||
|
||||
(
|
||||
(
|
||||
- df1_aaaaaaaaaaaa.merge()
|
||||
+ df1_aaaaaaaaaaaa
|
||||
+ .merge()
|
||||
.groupby(
|
||||
1,
|
||||
)
|
||||
@@ -228,7 +236,8 @@
|
||||
|
||||
(
|
||||
(
|
||||
- df1_aaaaaaaaaaaa.merge()
|
||||
+ df1_aaaaaaaaaaaa
|
||||
+ .merge()
|
||||
.groupby(
|
||||
1,
|
||||
)
|
||||
@@ -255,19 +264,19 @@
|
||||
|
||||
|
||||
expr = (
|
||||
- pl.scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
- .filter(
|
||||
- [
|
||||
- pl.col("path").str.contains(
|
||||
- r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
- ),
|
||||
- ~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
- ~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
- ]
|
||||
- )
|
||||
+ pl
|
||||
+ .scan_parquet("/data/pypi-parquet/*.parquet")
|
||||
+ .filter([
|
||||
+ pl.col("path").str.contains(
|
||||
+ r"\.(asm|c|cc|cpp|cxx|h|hpp|rs|[Ff][0-9]{0,2}(?:or)?|go)$"
|
||||
+ ),
|
||||
+ ~pl.col("path").str.contains(r"(^|/)test(|s|ing)"),
|
||||
+ ~pl.col("path").str.contains("/site-packages/", literal=True),
|
||||
+ ])
|
||||
.with_columns(
|
||||
month=pl.col("uploaded_on").dt.truncate("1mo"),
|
||||
- ext=pl.col("path")
|
||||
+ ext=pl
|
||||
+ .col("path")
|
||||
.str.extract(pattern=r"\.([a-z0-9]+)$", group_index=1)
|
||||
.str.replace_all(pattern=r"cxx|cpp|cc|c|hpp|h", value="C/C++")
|
||||
.str.replace_all(pattern="^f.*$", value="Fortran")
|
||||
@@ -288,9 +297,8 @@
|
||||
if more_nested_because_line_length:
|
||||
identical_hidden_layer_sizes = all(
|
||||
current_hidden_layer_sizes == first_hidden_layer_sizes
|
||||
- for current_hidden_layer_sizes in self.component_config[
|
||||
- HIDDEN_LAYERS_SIZES
|
||||
- ]
|
||||
+ for current_hidden_layer_sizes in self
|
||||
+ .component_config[HIDDEN_LAYERS_SIZES]
|
||||
.values()
|
||||
.attr
|
||||
)
|
||||
@@ -302,7 +310,8 @@
|
||||
with self.read_ctx(book_type) as cursor:
|
||||
if (
|
||||
entry_count := len(
|
||||
- names := cursor.execute(
|
||||
+ names := cursor
|
||||
+ .execute(
|
||||
"SELECT name FROM address_book WHERE address=?",
|
||||
(address,),
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue