From e465bfcb0f82c2c51f9b47cfb7cc228944c40c2b Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 30 Nov 2023 09:58:50 +0800 Subject: [PATCH] Prefer splitting right hand side of assignments --- .../assignment_split_value_first.options.json | 5 + .../statement/assignment_split_value_first.py | 71 +++ .../src/expression/mod.rs | 2 +- crates/ruff_python_formatter/src/preview.rs | 7 + .../src/statement/stmt_ann_assign.rs | 44 +- .../src/statement/stmt_assign.rs | 529 +++++++++++++++--- .../src/statement/stmt_aug_assign.rs | 47 +- ...es__pep604_union_types_line_breaks.py.snap | 20 +- ...bility@cases__preview_long_strings.py.snap | 16 +- ...__preview_long_strings__regression.py.snap | 34 +- ...ty@cases__preview_prefer_rhs_split.py.snap | 155 +---- ...ion__optional_parentheses_comments.py.snap | 40 ++ .../tests/snapshots/format@preview.py.snap | 30 +- .../format@statement__ann_assign.py.snap | 20 + .../format@statement__assign.py.snap | 46 ++ ...ment__assignment_split_value_first.py.snap | 154 +++++ 16 files changed, 924 insertions(+), 296 deletions(-) create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.options.json new file mode 100644 index 00000000000000..8925dd0a8280f7 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.options.json @@ -0,0 +1,5 @@ +[ + { + "preview": "enabled" + } +] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py new file mode 100644 index 00000000000000..846f5c75faf925 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py @@ -0,0 +1,71 @@ +# Don't parenthesize the value because the target's trailing comma forces it to split. +a[ + aaaaaaa, + b, +] = cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment + +# Parenthesize the value, but don't duplicate the comment. +a[ + aaaaaaa, + b +] = cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment + +# Format both as flat, but don't loos the comment. +a[ + aaaaaaa, + b +] = bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # comment + +####################################################### +# Test the case where a parenthesized value now fits: +a[ + aaaaaaa, + b +] = ( + cccccccc # comment +) + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +a[ + aaaaaaa, + b, +] = ( + cccccccc # comment +) + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +# The group breaks because of its comments +a[ + aaaaaaa, + b +] = ( + # leading comment + b +) = ( + cccccccc # comment +) + + +a[bbbbbbbbbbbbbbbbbb] = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + +# Does not double-parenthesize tuples +( + first_item, + second_item, +) = some_looooooooong_module.some_loooooog_function_name( + first_argument, second_argument, third_argument +) + + +# Preserve parentheses around the first target +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) = dst + +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) += dst diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 435c39dedcc256..084a138912b702 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -952,7 +952,7 @@ impl OwnParentheses { /// Differs from [`has_own_parentheses`] in that it returns [`OwnParentheses::NonEmpty`] for /// parenthesized expressions, like `(1)` or `([1])`, regardless of whether those expression have /// their _own_ parentheses. -fn has_parentheses(expr: &Expr, context: &PyFormatContext) -> Option { +pub(crate) fn has_parentheses(expr: &Expr, context: &PyFormatContext) -> Option { let own_parentheses = has_own_parentheses(expr, context); // If the node has its own non-empty parentheses, we don't need to check for surrounding diff --git a/crates/ruff_python_formatter/src/preview.rs b/crates/ruff_python_formatter/src/preview.rs index 8354e83a64d4aa..90379d1ad8fab8 100644 --- a/crates/ruff_python_formatter/src/preview.rs +++ b/crates/ruff_python_formatter/src/preview.rs @@ -17,3 +17,10 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled( ) -> bool { context.is_preview() } + +/// Returns `true` if the [`prefer_splitting_right_hand_side_of_assignments`](https://github.com/astral-sh/ruff/issues/6975) preview style is enabled. +pub(crate) const fn is_prefer_splitting_right_hand_side_of_assignments_enabled( + context: &PyFormatContext, +) -> bool { + context.is_preview() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index 17efcab1a8f77a..828b132485c59c 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -2,8 +2,12 @@ use ruff_formatter::write; use ruff_python_ast::StmtAnnAssign; use crate::comments::{SourceComment, SuppressionKind}; +use crate::expression::has_parentheses; use crate::prelude::*; -use crate::statement::stmt_assign::FormatStatementsLastExpression; +use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; +use crate::statement::stmt_assign::{ + AnyAssignmentOperator, FormatAssignmentValue, FormatStatementsLastExpression, +}; use crate::statement::trailing_semicolon; #[derive(Default)] @@ -19,21 +23,33 @@ impl FormatNodeRule for FormatStmtAnnAssign { simple: _, } = item; - write!( - f, - [target.format(), token(":"), space(), annotation.format(),] - )?; + write!(f, [target.format(), token(":"), space()])?; if let Some(value) = value { - write!( - f, - [ - space(), - token("="), - space(), - FormatStatementsLastExpression::new(value, item) - ] - )?; + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) + && has_parentheses(annotation, f.context()).is_some() + { + FormatAssignmentValue { + before_operator: annotation, + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } else { + write!( + f, + [ + annotation.format(), + space(), + token("="), + space(), + FormatStatementsLastExpression::new(value, item) + ] + )?; + } + } else { + annotation.format().fmt(f)?; } if f.options().source_type().is_ipynb() diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index a183e4e2ada25a..22990ecaa1ad05 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,13 +1,17 @@ use ruff_formatter::{format_args, write, FormatError}; -use ruff_python_ast::{AnyNodeRef, Expr, StmtAssign}; +use ruff_python_ast::{AnyNodeRef, Expr, Operator, StmtAssign}; -use crate::comments::{trailing_comments, SourceComment, SuppressionKind}; +use crate::builders::parenthesize_if_expands; +use crate::comments::{ + trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment, SuppressionKind, +}; use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{ - NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, + is_expression_parenthesized, NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, }; use crate::expression::{has_own_parentheses, maybe_parenthesize_expression}; use crate::prelude::*; +use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; use crate::statement::trailing_semicolon; #[derive(Default)] @@ -25,24 +29,52 @@ impl FormatNodeRule for FormatStmtAssign { "Expected at least on assignment target", ))?; - write!( - f, - [ - first.format(), - space(), - token("="), - space(), - FormatTargets { targets: rest } - ] - )?; + let format_first = + format_with(|f| write!(f, [first.format(), space(), token("="), space()])); + + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + if let Some((last, head)) = rest.split_last() { + format_first.fmt(f)?; + + for target in head { + FormatTarget { target }.fmt(f)?; + } - FormatStatementsLastExpression::new(value, item).fmt(f)?; + FormatAssignmentValue { + before_operator: last, + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } else if has_target_own_parentheses(first, f.context()) + && !is_expression_parenthesized( + first.into(), + f.context().comments().ranges(), + f.context().source(), + ) + { + FormatAssignmentValue { + before_operator: first, + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } else { + format_first.fmt(f)?; + FormatStatementsLastExpression::new(value, item).fmt(f)?; + } + } else { + write!(f, [format_first, FormatTargets { targets: rest }])?; + + FormatStatementsLastExpression::new(value, item).fmt(f)?; + } if f.options().source_type().is_ipynb() && f.context().node_level().is_last_top_level_statement() - && rest.is_empty() - && first.is_name_expr() && trailing_semicolon(item.into(), f.context().source()).is_some() + && matches!(targets.as_slice(), [Expr::Name(_)]) { token(";").fmt(f)?; } @@ -59,6 +91,29 @@ impl FormatNodeRule for FormatStmtAssign { } } +struct FormatTarget<'a> { + target: &'a Expr, +} + +impl Format> for FormatTarget<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if has_target_own_parentheses(self.target, f.context()) + && !f.context().comments().has_leading(self.target) + && !f.context().comments().has_trailing(self.target) + { + self.target + .format() + .with_options(Parentheses::Never) + .fmt(f)?; + } else { + parenthesize_if_expands(&self.target.format().with_options(Parentheses::Never)) + .fmt(f)?; + } + + write!(f, [space(), token("="), space()]) + } +} + #[derive(Debug)] struct FormatTargets<'a> { targets: &'a [Expr], @@ -71,7 +126,7 @@ impl Format> for FormatTargets<'_> { let parenthesize = if comments.has_leading(first) || comments.has_trailing(first) { ParenthesizeTarget::Always - } else if has_own_parentheses(first, f.context()).is_some() { + } else if has_target_own_parentheses(first, f.context()) { ParenthesizeTarget::Never } else { ParenthesizeTarget::IfBreaks @@ -179,22 +234,8 @@ impl<'a> FormatStatementsLastExpression<'a> { impl Format> for FormatStatementsLastExpression<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { - let can_inline_comment = match self.expression { - Expr::Name(_) - | Expr::NoneLiteral(_) - | Expr::NumberLiteral(_) - | Expr::BooleanLiteral(_) => true, - Expr::StringLiteral(string) => { - string.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit - } - Expr::BytesLiteral(bytes) => { - bytes.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit - } - Expr::FString(fstring) => { - fstring.needs_parentheses(self.parent, f.context()) == OptionalParentheses::BestFit - } - _ => false, - }; + let can_inline_comment = UsesBestFitLayout::new(self.expression, self.parent, f.context()) + .should_inline_comments(); if !can_inline_comment { return maybe_parenthesize_expression( @@ -208,70 +249,255 @@ impl Format> for FormatStatementsLastExpression<'_> { let comments = f.context().comments().clone(); let expression_comments = comments.leading_dangling_trailing(self.expression); - if expression_comments.has_leading() { - // Preserve the parentheses if the expression has any leading comments, + if let Some(inline_comments) = + OptionalParenthesesInlinedComments::new(&expression_comments, self.parent, &comments) + { + BestFitParenthesizeWithInlineComments { + inline_comments, + expression: self.expression, + } + .fmt(f) + } else { + // Preserve the parentheses if the expression has any leading or trailing comments, // same as `maybe_parenthesize_expression` - return self - .expression + self.expression .format() .with_options(Parentheses::Always) - .fmt(f); + .fmt(f) } + } +} - let statement_trailing_comments = comments.trailing(self.parent); - let after_end_of_line = statement_trailing_comments - .partition_point(|comment| comment.line_position().is_end_of_line()); - let (stmt_inline_comments, _) = statement_trailing_comments.split_at(after_end_of_line); - - let after_end_of_line = expression_comments - .trailing - .partition_point(|comment| comment.line_position().is_end_of_line()); - - let (expression_inline_comments, expression_trailing_comments) = - expression_comments.trailing.split_at(after_end_of_line); +/// Formats the last expression in statements that start with a keyword (like `return`) or after an operator (assignments). +/// +/// It avoids parenthesizing unsplittable values (like `None`, `True`, `False`, Names, a subset of strings) just to make +/// the trailing comment fit and inlines a trailing comment if the value itself exceeds the configured line width: +/// +/// The implementation formats the statement's and value's trailing end of line comments: +/// * after the expression if the expression needs no parentheses (necessary or the `expand_parent` makes the group never fit). +/// * inside the parentheses if the expression exceeds the line-width. +/// +/// ```python +/// a = loooooooooooooooooooooooooooong # with_comment +/// b = ( +/// short # with_comment +/// ) +/// ``` +/// +/// Which gets formatted to: +/// +/// ```python +/// # formatted +/// a = ( +/// loooooooooooooooooooooooooooong # with comment +/// ) +/// b = short # with comment +/// ``` +/// +/// The long name gets parenthesized because it exceeds the configured line width and the trailing comma of the +/// statement gets formatted inside (instead of outside) the parentheses. +/// +/// The `short` name gets unparenthesized because it fits into the configured line length, regardless of whether +/// the comment exceeds the line width or not. +/// +/// This logic isn't implemented in [`place_comment`] by associating trailing statement comments to the expression because +/// doing so breaks the suite empty lines formatting that relies on trailing comments to be stored on the statement. +pub(super) struct FormatAssignmentValue<'a> { + /// The expression that comes right before the assignment operator. This is either + /// the last target, or the annotated assignment type annotation. + pub(super) before_operator: &'a Expr, - if expression_trailing_comments.is_empty() { - let inline_comments = OptionalParenthesesInlinedComments { - expression: expression_inline_comments, - statement: stmt_inline_comments, - }; + /// The assignment operator. Either `Assign` (`=`) or the operator used by the augmented assignment statement. + pub(super) operator: AnyAssignmentOperator, - let group_id = f.group_id("optional_parentheses"); - let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + /// The value assigned to the target(s) + pub(super) value: &'a Expr, - best_fit_parenthesize(&format_with(|f| { - inline_comments.mark_formatted(); + /// The assignment statement. + pub(super) statement: AnyNodeRef<'a>, +} - self.expression +impl Format> for FormatAssignmentValue<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let format_before_operator = format_with(|f: &mut PyFormatter| { + // Preserve parentheses around targets with comments. + if f.context().comments().has_leading(self.before_operator) + || f.context().comments().has_trailing(self.before_operator) + { + self.before_operator.format().fmt(f) + } + // Never parenthesize targets that come with their own parentheses, e.g. don't parenthesize lists or dictionary literals. + else if has_target_own_parentheses(self.before_operator, f.context()) { + self.before_operator .format() .with_options(Parentheses::Never) - .fmt(f)?; + .fmt(f) + } else { + parenthesize_if_expands( + &self + .before_operator + .format() + .with_options(Parentheses::Never), + ) + .fmt(f) + } + }); - if !inline_comments.is_empty() { - // If the expressions exceeds the line width, format the comments in the parentheses - if_group_breaks(&inline_comments) - .with_group_id(Some(group_id)) - .fmt(f)?; - } + let uses_best_fit_layout = UsesBestFitLayout::new(self.value, self.statement, f.context()); - Ok(()) - })) - .with_group_id(Some(group_id)) - .fmt(f)?; + if uses_best_fit_layout.is_no() { + return write!( + f, + [ + format_before_operator, + space(), + self.operator, + space(), + maybe_parenthesize_expression( + self.value, + self.statement, + Parenthesize::IfBreaks + ) + ] + ); + } - if !inline_comments.is_empty() { - // If the line fits into the line width, format the comments after the parenthesized expression - if_group_fits_on_line(&inline_comments) - .with_group_id(Some(group_id)) - .fmt(f)?; - } + // Don't inline comments for attribute and call expressions for black compatibility + let should_inline_comments = uses_best_fit_layout.should_inline_comments(); - Ok(()) + let comments = f.context().comments().clone(); + let expression_comments = comments.leading_dangling_trailing(self.value); + let inline_comments = if should_inline_comments { + OptionalParenthesesInlinedComments::new(&expression_comments, self.statement, &comments) + } else if expression_comments.has_leading() || expression_comments.has_trailing_own_line() { + None } else { - self.expression - .format() - .with_options(Parentheses::Always) - .fmt(f) + Some(OptionalParenthesesInlinedComments::default()) + }; + + let Some(inline_comments) = inline_comments else { + // Preserve the parentheses if the expression has any leading or trailing own line comments + // same as `maybe_parenthesize_expression` + return write!( + f, + [ + format_before_operator, + space(), + self.operator, + space(), + self.value.format().with_options(Parentheses::Always) + ] + ); + }; + + // Prevent inline comments to be formatted as part of the expression. + inline_comments.mark_formatted(); + + let mut last_target = format_before_operator.memoized(); + + // Avoid using the `best fit` layout if it is known that the last target breaks + // This is mainly a perf improvement that avoids an additional memoization + // and using the costly `BestFit` layout if it is already known that the left breaks, + // because it would always pick the last best fitting variant. + if last_target.inspect(f)?.will_break() { + // Format the value without any parentheses. The check above guarantees that it never has leading comments. + return write!( + f, + [ + last_target, + space(), + self.operator, + space(), + self.value.format().with_options(Parentheses::Never), + inline_comments + ] + ); + } + + let format_value = self + .value + .format() + .with_options(Parentheses::Never) + .memoized(); + + // Try to fit the last assignment target and the value on a single line: + // ```python + // a = b = c + // ``` + let format_flat = format_with(|f| { + write!( + f, + [ + last_target, + space(), + self.operator, + space(), + format_value, + inline_comments + ] + ) + }); + + // Don't break the last assignment target but parenthesize the value to see if it fits + // ```python + // a["bbbbb"] = ( + // c + // ) + // ``` + let format_parenthesize_value = format_with(|f| { + write!( + f, + [ + last_target, + space(), + self.operator, + space(), + token("("), + block_indent(&format_args![format_value, inline_comments]), + token(")") + ] + ) + }); + + // Fall back to parenthesizing (or splitting) the last target part if we can't make the value + // fit. Don't parenthesize the value to avoid unnecessary parentheses. + // ```python + // a[ + // "bbbbb" + // ] = c + // ``` + let format_split_left = format_with(|f| { + write!( + f, + [ + last_target, + space(), + self.operator, + space(), + format_value, + inline_comments + ] + ) + }); + + // Call expression have one extra layout. + if self.value.is_call_expr() { + best_fitting![ + format_flat, + // Avoid parenthesizing the call expression if the `(` fit on the line + format_args![ + last_target, + space(), + self.operator, + space(), + group(&format_value).should_expand(true), + ], + format_parenthesize_value, + format_split_left + ] + .fmt(f) + } else { + best_fitting![format_flat, format_parenthesize_value, format_split_left].fmt(f) } } } @@ -283,6 +509,35 @@ struct OptionalParenthesesInlinedComments<'a> { } impl<'a> OptionalParenthesesInlinedComments<'a> { + fn new( + expression_comments: &LeadingDanglingTrailingComments<'a>, + statement: AnyNodeRef<'a>, + comments: &'a Comments<'a>, + ) -> Option { + if expression_comments.has_leading() || expression_comments.has_trailing_own_line() { + return None; + } + + let statement_trailing_comments = comments.trailing(statement); + let after_end_of_line = statement_trailing_comments + .partition_point(|comment| comment.line_position().is_end_of_line()); + let (stmt_inline_comments, _) = statement_trailing_comments.split_at(after_end_of_line); + + let after_end_of_line = expression_comments + .trailing + .partition_point(|comment| comment.line_position().is_end_of_line()); + + let (expression_inline_comments, trailing_own_line_comments) = + expression_comments.trailing.split_at(after_end_of_line); + + debug_assert!(trailing_own_line_comments.is_empty(), "The method should have returned early if the expression has trailing own line comments"); + + Some(OptionalParenthesesInlinedComments { + expression: expression_inline_comments, + statement: stmt_inline_comments, + }) + } + fn is_empty(&self) -> bool { self.expression.is_empty() && self.statement.is_empty() } @@ -313,3 +568,115 @@ impl Format> for OptionalParenthesesInlinedComments<'_> { ) } } + +#[derive(Copy, Clone, Debug)] +pub(super) enum AnyAssignmentOperator { + Assign, + AugAssign(Operator), +} + +impl Format> for AnyAssignmentOperator { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match self { + AnyAssignmentOperator::Assign => token("=").fmt(f), + AnyAssignmentOperator::AugAssign(operator) => { + write!(f, [operator.format(), token("=")]) + } + } + } +} + +struct BestFitParenthesizeWithInlineComments<'a> { + expression: &'a Expr, + inline_comments: OptionalParenthesesInlinedComments<'a>, +} + +impl Format> for BestFitParenthesizeWithInlineComments<'_> { + fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { + let group_id = f.group_id("optional_parentheses"); + + let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + + best_fit_parenthesize(&format_with(|f| { + self.inline_comments.mark_formatted(); + + self.expression + .format() + .with_options(Parentheses::Never) + .fmt(f)?; + + if !self.inline_comments.is_empty() { + // If the expressions exceeds the line width, format the comments in the parentheses + if_group_breaks(&self.inline_comments).fmt(f)?; + } + + Ok(()) + })) + .with_group_id(Some(group_id)) + .fmt(f)?; + + if !self.inline_comments.is_empty() { + // If the line fits into the line width, format the comments after the parenthesized expression + if_group_fits_on_line(&self.inline_comments) + .with_group_id(Some(group_id)) + .fmt(f)?; + } + + Ok(()) + } +} + +#[derive(Copy, Clone)] +enum UsesBestFitLayout<'a> { + Yes(&'a Expr), + No, +} + +impl<'a> UsesBestFitLayout<'a> { + fn new(expression: &'a Expr, parent: AnyNodeRef, context: &PyFormatContext) -> Self { + let uses_best_fit = match expression { + Expr::Name(_) + | Expr::NoneLiteral(_) + | Expr::NumberLiteral(_) + | Expr::BooleanLiteral(_) => true, + Expr::StringLiteral(string) => { + string.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + Expr::BytesLiteral(bytes) => { + bytes.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + Expr::FString(fstring) => { + fstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + Expr::Attribute(attribute) => { + attribute.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + Expr::Call(call) => { + call.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + _ => false, + }; + + if uses_best_fit { + Self::Yes(expression) + } else { + Self::No + } + } + + fn should_inline_comments(self) -> bool { + match self { + UsesBestFitLayout::Yes(Expr::Attribute(_) | Expr::Call(_)) => false, + UsesBestFitLayout::Yes(_) => true, + UsesBestFitLayout::No => false, + } + } + + const fn is_no(self) -> bool { + matches!(self, Self::No) + } +} + +pub(super) fn has_target_own_parentheses(target: &Expr, context: &PyFormatContext) -> bool { + matches!(target, Expr::Tuple(_)) || has_own_parentheses(target, context).is_some() +} diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index 19202ecf0f9829..7743524ba5b712 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -2,8 +2,13 @@ use ruff_formatter::write; use ruff_python_ast::StmtAugAssign; use crate::comments::{SourceComment, SuppressionKind}; +use crate::expression::parentheses::is_expression_parenthesized; use crate::prelude::*; -use crate::statement::stmt_assign::FormatStatementsLastExpression; +use crate::preview::is_prefer_splitting_right_hand_side_of_assignments_enabled; +use crate::statement::stmt_assign::{ + has_target_own_parentheses, AnyAssignmentOperator, FormatAssignmentValue, + FormatStatementsLastExpression, +}; use crate::statement::trailing_semicolon; use crate::{AsFormat, FormatNodeRule}; @@ -18,17 +23,35 @@ impl FormatNodeRule for FormatStmtAugAssign { value, range: _, } = item; - write!( - f, - [ - target.format(), - space(), - op.format(), - token("="), - space(), - FormatStatementsLastExpression::new(value, item) - ] - )?; + + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) + && has_target_own_parentheses(target, f.context()) + && !is_expression_parenthesized( + target.into(), + f.context().comments().ranges(), + f.context().source(), + ) + { + FormatAssignmentValue { + before_operator: target, + operator: AnyAssignmentOperator::AugAssign(*op), + value, + statement: item.into(), + } + .fmt(f)?; + } else { + write!( + f, + [ + target.format(), + space(), + op.format(), + token("="), + space(), + FormatStatementsLastExpression::new(value, item) + ] + )?; + } if f.options().source_type().is_ipynb() && f.context().node_level().is_last_top_level_statement() diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap index 278a20e4a66501..bf6f676be094e3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep604_union_types_line_breaks.py.snap @@ -95,7 +95,7 @@ def f( ```diff --- Black +++ Ruff -@@ -7,26 +7,16 @@ +@@ -7,23 +7,13 @@ ) # "AnnAssign"s now also work @@ -120,16 +120,10 @@ def f( - | Loooooooooooooooooooooooong - | Loooooooooooooooooooooooong -) = 7 --z: Short | Short2 | Short3 | Short4 = 8 --z: int = 2.3 --z: int = foo() +z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 -+z: (Short | Short2 | Short3 | Short4) = 8 -+z: (int) = 2.3 -+z: (int) = foo() - - # In case I go for not enforcing parantheses, this might get improved at the same time - x = ( + z: Short | Short2 | Short3 | Short4 = 8 + z: int = 2.3 + z: int = foo() @@ -63,7 +53,7 @@ @@ -186,9 +180,9 @@ z: (int) z: Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong | Loooooooooooooooooooooooong = 7 -z: (Short | Short2 | Short3 | Short4) = 8 -z: (int) = 2.3 -z: (int) = foo() +z: Short | Short2 | Short3 | Short4 = 8 +z: int = 2.3 +z: int = foo() # In case I go for not enforcing parantheses, this might get improved at the same time x = ( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap index b3a03820fa4cad..a589aeffed92e2 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings.py.snap @@ -789,14 +789,12 @@ log.info(f"""Skipping: {'a' == 'b'} {desc['ms_name']} {money=} {dte=} {pos_share - "This is a large string that has a type annotation attached to it. A type" - " annotation should NOT stop a long string from being wrapped." -) --annotated_variable: Literal["fakse_literal"] = ( ++annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + annotated_variable: Literal["fakse_literal"] = ( - "This is a large string that has a type annotation attached to it. A type" - " annotation should NOT stop a long string from being wrapped." --) -+annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -+annotated_variable: Literal[ -+ "fakse_literal" -+] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." ++ "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." + ) -backslashes = ( - "This is a really long string with \"embedded\" double quotes and 'single' quotes" @@ -1308,9 +1306,9 @@ annotated_variable: Final = ( + "using the '+' operator." ) annotated_variable: Final = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." -annotated_variable: Literal[ - "fakse_literal" -] = "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +annotated_variable: Literal["fakse_literal"] = ( + "This is a large string that has a type annotation attached to it. A type annotation should NOT stop a long string from being wrapped." +) backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\" backslashes = "This is a really long string with \"embedded\" double quotes and 'single' quotes that also handles checking for an even number of backslashes \\\\" diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap index 1cde924609d48b..762af6aa16c7ee 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_long_strings__regression.py.snap @@ -832,7 +832,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: some_commented_string = ( # This comment stays at the top. "This string is long but not so long that it needs hahahah toooooo be so greatttt" -@@ -279,38 +280,27 @@ +@@ -279,36 +280,25 @@ ) lpar_and_rpar_have_comments = func_call( # LPAR Comment @@ -852,33 +852,31 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: - f" {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -) +cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -+ -+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -cmd_fstring = ( - "sudo -E deluge-console info --detailed --sort-reverse=time_added" - f" {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -) -+cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'" ++cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {'{{}}' if ID is None else ID} | perl -nE 'print if /^{field}:/'" -cmd_fstring = ( - "sudo -E deluge-console info --detailed --sort-reverse=time_added {'' if ID is" - f" None else ID}} | perl -nE 'print if /^{field}:/'" -) -+fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}." ++cmd_fstring = f"sudo -E deluge-console info --detailed --sort-reverse=time_added {{'' if ID is None else ID}} | perl -nE 'print if /^{field}:/'" ++fstring = f"This string really doesn't need to be an {{{{fstring}}}}, but this one most certainly, absolutely {does}." ++ fstring = ( - "This string really doesn't need to be an {{fstring}}, but this one most" - f" certainly, absolutely {does}." + f"We have to remember to escape {braces}." " Like {these}." f" But not {this}." ) - +- -fstring = f"We have to remember to escape {braces}. Like {{these}}. But not {this}." -- + class A: - class B: - def foo(): @@ -364,10 +354,7 @@ def foo(): if not hasattr(module, name): @@ -933,7 +931,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: ) -@@ -432,14 +415,12 @@ +@@ -432,9 +415,7 @@ assert xxxxxxx_xxxx in [ x.xxxxx.xxxxxx.xxxxx.xxxxxx, x.xxxxx.xxxxxx.xxxxx.xxxx, @@ -943,15 +941,7 @@ s = f'Lorem Ipsum is simply dummy text of the printing and typesetting industry: + ], "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx --value.__dict__[key] = ( -- "test" # set some Thrift field to non-None in the struct aa bb cc dd ee --) -+value.__dict__[ -+ key -+] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee - - RE_ONE_BACKSLASH = { - "asdf_hjkl_jkl": re.compile( + value.__dict__[key] = ( @@ -449,8 +430,7 @@ RE_TWO_BACKSLASHES = { @@ -1627,9 +1617,9 @@ class xxxxxxxxxxxxxxxxxxxxx(xxxx.xxxxxxxxxxxxx): ], "xxxxxxxxxxx xxxxxxx xxxx (xxxxxx xxxx) %x xxx xxxxx" % xxxxxxx_xxxx -value.__dict__[ - key -] = "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) RE_ONE_BACKSLASH = { "asdf_hjkl_jkl": re.compile( diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap index 18b9fa9a062682..e799efc3145f69 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__preview_prefer_rhs_split.py.snap @@ -118,57 +118,7 @@ a = ( ```diff --- Black +++ Ruff -@@ -1,29 +1,31 @@ --first_item, second_item = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+( -+ first_item, -+ second_item, -+) = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - --some_dict["with_a_long_key"] = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+some_dict[ -+ "with_a_long_key" -+] = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - - # Make sure it works when the RHS only has one pair of (optional) parens. --first_item, second_item = ( -- some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name --) -+( -+ first_item, -+ second_item, -+) = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name - --some_dict["with_a_long_key"] = ( -- some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name --) -+some_dict[ -+ "with_a_long_key" -+] = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name - - # Make sure chaining assignments work. --first_item, second_item, third_item, forth_item = m["everything"] = ( -- some_looooooooong_module.some_looooooooooooooong_function_name( -- first_argument, second_argument, third_argument -- ) -+first_item, second_item, third_item, forth_item = m[ -+ "everything" -+] = some_looooooooong_module.some_looooooooooooooong_function_name( -+ first_argument, second_argument, third_argument - ) - - # Make sure when the RHS's first split at the non-optional paren fits, -@@ -60,9 +62,7 @@ +@@ -60,9 +60,7 @@ some_arg ).intersection(pk_cols) @@ -179,76 +129,37 @@ a = ( some_kind_of_table[ some_key # type: ignore # noqa: E501 -@@ -85,15 +85,29 @@ - ) - - # Multiple targets --a = b = ( -- ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc --) -+a = ( -+ b -+) = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc - --a = b = c = d = e = f = g = ( -+a = ( -+ b -+) = ( -+ c -+) = ( -+ d -+) = ( -+ e -+) = ( -+ f -+) = ( -+ g -+) = ( - hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh --) = i = j = ( -- kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk --) -+) = ( -+ i -+) = ( -+ j -+) = kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk - - a = ( - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ``` ## Ruff Output ```python -( - first_item, - second_item, -) = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +first_item, second_item = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +some_dict["with_a_long_key"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) # Make sure it works when the RHS only has one pair of (optional) parens. -( - first_item, - second_item, -) = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +first_item, second_item = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) -some_dict[ - "with_a_long_key" -] = some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +some_dict["with_a_long_key"] = ( + some_looooooooong_module.SomeClass.some_looooooooooooooong_variable_name +) # Make sure chaining assignments work. -first_item, second_item, third_item, forth_item = m[ - "everything" -] = some_looooooooong_module.some_looooooooooooooong_function_name( - first_argument, second_argument, third_argument +first_item, second_item, third_item, forth_item = m["everything"] = ( + some_looooooooong_module.some_looooooooooooooong_function_name( + first_argument, second_argument, third_argument + ) ) # Make sure when the RHS's first split at the non-optional paren fits, @@ -308,29 +219,15 @@ some_kind_of_instance.some_kind_of_map[a_key] = ( ) # Multiple targets -a = ( - b -) = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +a = b = ( + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) -a = ( - b -) = ( - c -) = ( - d -) = ( - e -) = ( - f -) = ( - g -) = ( +a = b = c = d = e = f = g = ( hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh -) = ( - i -) = ( - j -) = kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) = i = j = ( + kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk +) a = ( bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap index 4279cd03f29dba..23c95ef3a27564 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__optional_parentheses_comments.py.snap @@ -421,4 +421,44 @@ def test6(): ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -72,13 +72,13 @@ + ## Breaking left + + # Should break `[a]` first +-____[ +- a +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++____[a] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # c ++) + +-____[ +- a +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # cc ++____[a] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvv # cc ++) + + ( + # some weird comments +@@ -136,9 +136,9 @@ + # 89 characters parenthesized (collapse) + ____a: a = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c + +-_a: a[ +- b +-] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++_a: a[b] = ( ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvv # c ++) + + ## Augmented Assign + +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap index c3504c47f856c5..8fd73124467f4e 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -202,17 +202,17 @@ class RemoveNewlineBeforeClassDocstring: def f(): """Black's `Preview.prefer_splitting_right_hand_side_of_assignments`""" - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc.cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc.cccccccc + ) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc().cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc().cccccccc + ) - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[ - bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - ] = cccccccc.ccccccccccccc(d).cccccccc + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( + cccccccc.ccccccccccccc(d).cccccccc + ) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb] = ( cccccccc.ccccccccccccc(d).cccccccc + e @@ -226,12 +226,12 @@ def f(): + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ) - self._cache: dict[ - DependencyCacheKey, list[list[DependencyPackage]] - ] = collections.defaultdict(list) - self._cached_dependencies_by_level: dict[ - int, list[DependencyCacheKey] - ] = collections.defaultdict(list) + self._cache: dict[DependencyCacheKey, list[list[DependencyPackage]]] = ( + collections.defaultdict(list) + ) + self._cached_dependencies_by_level: dict[int, list[DependencyCacheKey]] = ( + collections.defaultdict(list) + ) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap index 69490f3caf53c9..191b6d141bb42a 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__ann_assign.py.snap @@ -67,4 +67,24 @@ class DefaultRunner: ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -7,9 +7,9 @@ + Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() + ) + +-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: ( +- Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +-) = Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() ++bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb: Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( ++ Bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb() ++) + + JSONSerializable: TypeAlias = ( + "str | int | float | bool | None | list | tuple | JSONMapping" +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap index 203d43b643802f..1c01bdbb3b3ec8 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assign.py.snap @@ -169,4 +169,50 @@ c = b[dddddd, aaaaaa] = ( ``` +## Preview changes +```diff +--- Stable ++++ Preview +@@ -1,7 +1,5 @@ + # break left hand side +-a1akjdshflkjahdslkfjlasfdahjlfds = ( +- bakjdshflkjahdslkfjlasfdahjlfds +-) = ( ++a1akjdshflkjahdslkfjlasfdahjlfds = bakjdshflkjahdslkfjlasfdahjlfds = ( + cakjdshflkjahdslkfjlasfdahjlfds + ) = kjaödkjaföjfahlfdalfhaöfaöfhaöfha = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = 3 + +@@ -9,15 +7,13 @@ + a2 = b2 = 2 + + # Break the last element +-a = ( +- asdf +-) = ( ++a = asdf = ( + fjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfalflaflapamsakjsdhflakjdslfjhalsdljfal + ) = 1 + +-aa = [ +- bakjdshflkjahdslkfjlasfdahjlfds +-] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] ++aa = [bakjdshflkjahdslkfjlasfdahjlfds] = dddd = ddd = ( ++ fkjaödkjaföjfahlfdalfhaöfaöfhaöfha ++) = g = [3] + + aa = [] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] + +@@ -27,7 +23,8 @@ + + aa = [] = dddd = ddd = fkjaödkjaföjfahlfdalfhaöfaöfhaöfha = g = [3] + +-aaaa = ( # trailing ++aaaa = ( ++ # trailing + # comment + bbbbb + ) = cccccccccccccccc = 3 +``` + + diff --git a/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap new file mode 100644 index 00000000000000..c59ffd3c8f48f8 --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap @@ -0,0 +1,154 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py +--- +## Input +```python +# Don't parenthesize the value because the target's trailing comma forces it to split. +a[ + aaaaaaa, + b, +] = cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment + +# Parenthesize the value, but don't duplicate the comment. +a[ + aaaaaaa, + b +] = cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment + +# Format both as flat, but don't loos the comment. +a[ + aaaaaaa, + b +] = bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # comment + +####################################################### +# Test the case where a parenthesized value now fits: +a[ + aaaaaaa, + b +] = ( + cccccccc # comment +) + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +a[ + aaaaaaa, + b, +] = ( + cccccccc # comment +) + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +# The group breaks because of its comments +a[ + aaaaaaa, + b +] = ( + # leading comment + b +) = ( + cccccccc # comment +) + + +a[bbbbbbbbbbbbbbbbbb] = ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc + +# Does not double-parenthesize tuples +( + first_item, + second_item, +) = some_looooooooong_module.some_loooooog_function_name( + first_argument, second_argument, third_argument +) + + +# Preserve parentheses around the first target +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) = dst + +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) += dst +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +preview = Enabled +``` + +```python +# Don't parenthesize the value because the target's trailing comma forces it to split. +a[ + aaaaaaa, + b, +] = cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment + +# Parenthesize the value, but don't duplicate the comment. +a[aaaaaaa, b] = ( + cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc # comment +) + +# Format both as flat, but don't loos the comment. +a[aaaaaaa, b] = bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # comment + +####################################################### +# Test the case where a parenthesized value now fits: +a[aaaaaaa, b] = cccccccc # comment + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +a[ + aaaaaaa, + b, +] = cccccccc # comment + +# Doesn't use `BestFit` because the target always breaks because of the trailing comma +# The group breaks because of its comments +a[aaaaaaa, b] = ( + # leading comment + b +) = cccccccc # comment + + +a[bbbbbbbbbbbbbbbbbb] = ( + ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc +) + +# Does not double-parenthesize tuples +( + first_item, + second_item, +) = some_looooooooong_module.some_loooooog_function_name( + first_argument, second_argument, third_argument +) + + +# Preserve parentheses around the first target +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) = dst + +( + req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" + ]["destinations"]["destination"][0]["ip_address"] +) += dst +``` + + +