diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py index c5d806e27e..0b49a1dd11 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/parentheses/call_chains.py @@ -161,3 +161,58 @@ max_message_id = ( max_message_id = ( Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id() ) + +# Parentheses with fluent style within and outside of the parentheses. +( + ( + df1_aaaaaaaaaaaa.merge() + ) + .groupby(1,) + .sum() +) + +( + ( # foo + df1_aaaaaaaaaaaa.merge() + ) + .groupby(1,) + .sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + ) + .sum() +) + + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + ) + .sum() + .bar() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + .bar() + ) + .sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + .bar() + ) + .sum() + .baz() +) + diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 047e88d980..3b11cde300 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -37,49 +37,45 @@ impl FormatNodeRule for FormatExprAttribute { let call_chain_layout = self.call_chain_layout.apply_in_node(item, f); let format_inner = format_with(|f: &mut PyFormatter| { - let needs_parentheses = matches!( - value.as_ref(), - Expr::Constant(ExprConstant { - value: Constant::Int(_) | Constant::Float(_), - .. - }) - ); + let parenthesize_value = + // If the value is an integer, we need to parenthesize it to avoid a syntax error. + matches!( + value.as_ref(), + Expr::Constant(ExprConstant { + value: Constant::Int(_) | Constant::Float(_), + .. + }) + ) || is_expression_parenthesized(value.into(), f.context().source()); - if needs_parentheses { - value.format().with_options(Parentheses::Always).fmt(f)?; - } else if call_chain_layout == CallChainLayout::Fluent { - match value.as_ref() { - Expr::Attribute(expr) => { - expr.format().with_options(call_chain_layout).fmt(f)?; - } - Expr::Call(expr) => { - expr.format().with_options(call_chain_layout).fmt(f)?; - if call_chain_layout == CallChainLayout::Fluent { - // Format the dot on its own line + if call_chain_layout == CallChainLayout::Fluent { + if parenthesize_value { + // Don't propagate the call chain layout. + value.format().with_options(Parentheses::Always).fmt(f)?; + + // Format the dot on its own line. + soft_line_break().fmt(f)?; + } else { + match value.as_ref() { + Expr::Attribute(expr) => { + expr.format().with_options(call_chain_layout).fmt(f)?; + } + Expr::Call(expr) => { + expr.format().with_options(call_chain_layout).fmt(f)?; soft_line_break().fmt(f)?; } - } - Expr::Subscript(expr) => { - expr.format().with_options(call_chain_layout).fmt(f)?; - if call_chain_layout == CallChainLayout::Fluent { - // Format the dot on its own line + Expr::Subscript(expr) => { + expr.format().with_options(call_chain_layout).fmt(f)?; soft_line_break().fmt(f)?; } - } - _ => { - // This matches [`CallChainLayout::from_expression`] - if is_expression_parenthesized(value.as_ref().into(), f.context().source()) - { - value.format().with_options(Parentheses::Always).fmt(f)?; - // Format the dot on its own line - soft_line_break().fmt(f)?; - } else { - value.format().fmt(f)?; + _ => { + value.format().with_options(Parentheses::Never).fmt(f)?; } } } + } else if parenthesize_value { + value.format().with_options(Parentheses::Always).fmt(f)?; } else { - value.format().fmt(f)?; + value.format().with_options(Parentheses::Never).fmt(f)?; } // Identify dangling comments before and after the dot: diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 91e2e54065..a3b798ae09 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -630,20 +630,21 @@ impl CallChainLayout { loop { match expr { ExpressionRef::Attribute(ast::ExprAttribute { value, .. }) => { - expr = ExpressionRef::from(value.as_ref()); // ``` // f().g // ^^^ value // data[:100].T // ^^^^^^^^^^ value // ``` - if matches!(value.as_ref(), Expr::Call(_) | Expr::Subscript(_)) { - attributes_after_parentheses += 1; - } else if is_expression_parenthesized(expr, source) { + if is_expression_parenthesized(value.into(), source) { // `(a).b`. We preserve these parentheses so don't recurse attributes_after_parentheses += 1; break; + } else if matches!(value.as_ref(), Expr::Call(_) | Expr::Subscript(_)) { + attributes_after_parentheses += 1; } + + expr = ExpressionRef::from(value.as_ref()); } // ``` // f() @@ -666,9 +667,11 @@ impl CallChainLayout { if is_expression_parenthesized(expr, source) { attributes_after_parentheses += 1; } + break; } } + // We preserve these parentheses so don't recurse if is_expression_parenthesized(expr, source) { break; diff --git a/crates/ruff_python_formatter/tests/snapshots/format@parentheses__call_chains.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@parentheses__call_chains.py.snap index 1bd2089f6b..e448447ba1 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@parentheses__call_chains.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@parentheses__call_chains.py.snap @@ -167,6 +167,61 @@ max_message_id = ( max_message_id = ( Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id() ) + +# Parentheses with fluent style within and outside of the parentheses. +( + ( + df1_aaaaaaaaaaaa.merge() + ) + .groupby(1,) + .sum() +) + +( + ( # foo + df1_aaaaaaaaaaaa.merge() + ) + .groupby(1,) + .sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + ) + .sum() +) + + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + ) + .sum() + .bar() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + .bar() + ) + .sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby(1,) + .bar() + ) + .sum() + .baz() +) + ``` ## Output @@ -350,6 +405,66 @@ max_message_id = ( max_message_id = ( Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id() ) + +# Parentheses with fluent style within and outside of the parentheses. +( + (df1_aaaaaaaaaaaa.merge()) + .groupby( + 1, + ) + .sum() +) + +( + ( # foo + df1_aaaaaaaaaaaa.merge() + ) + .groupby( + 1, + ) + .sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge().groupby( + 1, + ) + ).sum() +) + + +( + ( + df1_aaaaaaaaaaaa.merge().groupby( + 1, + ) + ) + .sum() + .bar() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby( + 1, + ) + .bar() + ).sum() +) + +( + ( + df1_aaaaaaaaaaaa.merge() + .groupby( + 1, + ) + .bar() + ) + .sum() + .baz() +) ```