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 0000000000000..8925dd0a8280f --- /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 0000000000000..4c266a1ad50ee --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assignment_split_value_first.py @@ -0,0 +1,218 @@ +####### +# Unsplittable target and value + +# Only parenthesize the value if it makes it fit, otherwise avoid parentheses. +b = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +bbbbbbbbbbbbbbbb = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv + +# Avoid parenthesizing the value even if the target exceeds the configured width +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = bbb + + +############ +# Splittable targets + +# 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 + +# Augmented assignment +req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" +] += dst + +# Always parenthesize the value if it avoids splitting the target, regardless of the value's width. +_a: a[aaaa] = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +) + +##### +# Avoid parenthesizing the value if the expression right before the `=` splits to avoid an unnecessary pair of parentheses + +# The type annotation is guaranteed to split because it is too long. +_a: a[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target is too long +( + aaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target splits because of a magic trailing comma +( + a, + b, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +# The targets split because of a comment +( + # leading + a +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a + # trailing +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a, # nested + b +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +####### +# Multi targets + +# Black always parenthesizes the right if using multiple targets regardless if the parenthesized value exceeds the +# the configured line width or not +aaaa = bbbbbbbbbbbbbbbb = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee +) + +# Black does parenthesize the target if the target itself exceeds the line width and only parenthesizes +# the values if it makes it fit. +# The second target is too long to ever fit into the configured line width. +aaaa = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdddd +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +# Does also apply for other multi target assignments, as soon as a single target exceeds the configured +# width +aaaaaa = a["aaa"] = bbbbb[aa, bbb, cccc] = dddddddddd = eeeeee = ( + fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + +###################### +# Call expressions: +# For unsplittable targets: Parenthesize the call expression if it makes it fit. +# +# For splittable targets: +# Only parenthesize a call expression if the parens of the call don't fit on the same line +# as the target. Don't parenthesize the call expression if the target (or annotation) right before +# splits. + +# Don't parenthesize the function call if the left is unsplittable. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = a.b.function( + arg1, arg2, arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = ( + function() +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = ( + a.b.function(arg1, arg2, arg3) +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function() +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +####### Fluent call expressions +# Uses the regular `Multiline` layout where the entire `value` gets parenthesized +# if it doesn't fit on the line. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use = ( + function().b().c([1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3) +) + + +####### +# Test comment inlining +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) + + +# 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 +) + +# Splits the target but not the value because of the magic trailing comma. +a[ + aaaaaaa, + b, +] = ( + cccccccc # comment +) + +# Splits the second target because of the comment and the first target because of the trailing comma. +a[ + aaaaaaa, + b, +] = ( + # leading comment + b +) = ( + cccccccc # comment +) + + +######## +# Type Alias Statement +type A[str, int, number] = VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtin + +type A[VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtinthatExceedsTheWidth] = str + diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 435c39dedcc25..084a138912b70 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 8354e83a64d4a..90379d1ad8fab 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 17efcab1a8f77..89a97acc6f454 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, AnyBeforeOperator, 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() + { + FormatStatementsLastExpression::RightToLeft { + before_operator: AnyBeforeOperator::Expression(annotation), + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } else { + write!( + f, + [ + annotation.format(), + space(), + token("="), + space(), + FormatStatementsLastExpression::left_to_right(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 a183e4e2ada25..9430e7289a210 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, TypeParams}; -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,66 @@ impl FormatNodeRule for FormatStmtAssign { "Expected at least on assignment target", ))?; - write!( - f, - [ - first.format(), - space(), - token("="), - space(), - FormatTargets { targets: rest } - ] - )?; + // The first target is special because it never gets parenthesized nor does the formatter remove parentheses if unnecessary. + let format_first = FormatTargetWithEqualOperator { + target: first, + preserve_parentheses: true, + }; - FormatStatementsLastExpression::new(value, item).fmt(f)?; + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + // Avoid parenthesizing the value if the last target before the assigned value expands. + if let Some((last, head)) = rest.split_last() { + format_first.fmt(f)?; + + for target in head { + FormatTargetWithEqualOperator { + target, + preserve_parentheses: false, + } + .fmt(f)?; + } + + FormatStatementsLastExpression::RightToLeft { + before_operator: AnyBeforeOperator::Expression(last), + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } + // Avoid parenthesizing the value for single-target assignments that where the + // target has its own parentheses (list, dict, tuple, ...) and the target expands. + else if has_target_own_parentheses(first, f.context()) + && !is_expression_parenthesized( + first.into(), + f.context().comments().ranges(), + f.context().source(), + ) + { + FormatStatementsLastExpression::RightToLeft { + before_operator: AnyBeforeOperator::Expression(first), + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f)?; + } + // For single targets that have no split points, parenthesize the value only + // if it makes it fit. Otherwise omit the parentheses. + else { + format_first.fmt(f)?; + FormatStatementsLastExpression::left_to_right(value, item).fmt(f)?; + } + } else { + write!(f, [format_first, FormatTargets { targets: rest }])?; + + FormatStatementsLastExpression::left_to_right(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 +105,7 @@ impl FormatNodeRule for FormatStmtAssign { } } +/// Formats the targets so that they split left-to right. #[derive(Debug)] struct FormatTargets<'a> { targets: &'a [Expr], @@ -71,7 +118,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 @@ -129,10 +176,50 @@ enum ParenthesizeTarget { IfBreaks, } +/// Formats a single target with the equal operator. +struct FormatTargetWithEqualOperator<'a> { + target: &'a Expr, + + /// Whether parentheses should be preserved as in the source or if the target + /// should only be parenthesized if necessary (because of comments or because it doesn't fit). + preserve_parentheses: bool, +} + +impl Format> for FormatTargetWithEqualOperator<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + // Preserve parentheses for the first target or around targets with leading or trailing comments. + if self.preserve_parentheses + || f.context().comments().has_leading(self.target) + || f.context().comments().has_trailing(self.target) + { + self.target.format().fmt(f)?; + } else if has_target_own_parentheses(self.target, f.context()) { + 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()]) + } +} + /// 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 avoids parenthesizing unsplittable values (like `None`, `True`, `False`, Names, a subset of strings) +/// if the value won't fit even when parenthesized. +/// +/// ## Trailing comments +/// Trailing comments are inlined inside the `value`'s parentheses rather than formatted at the end +/// of the statement for unsplittable values if the `value` gets parenthesized. +/// +/// Inlining the trailing comments prevent situations where the parenthesized value +/// still exceeds the configured line width, but parenthesizing helps to make the trailing comment fit. +/// Instead, it only parenthesizes `value` if it makes both the `value` and the trailing comment fit. +/// See [PR 8431](https://github.com/astral-sh/ruff/pull/8431) for more details. /// /// 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). @@ -155,123 +242,337 @@ enum ParenthesizeTarget { /// b = short # with comment /// ``` /// -/// The long name gets parenthesized because it exceeds the configured line width and the trailing comma of the +/// The long name gets parenthesized because it exceeds the configured line width and the trailing comment 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 +/// No parentheses are added for `short` 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 FormatStatementsLastExpression<'a> { - expression: &'a Expr, - parent: AnyNodeRef<'a>, +pub(super) enum FormatStatementsLastExpression<'a> { + /// Prefers to split what's left of `value` before splitting the value. + /// + /// ```python + /// aaaaaaa[bbbbbbbb] = some_long_value + /// ``` + /// + /// This layout splits `aaaaaaa[bbbbbbbb]` first assuming the whole statements exceeds the line width, resulting in + /// + /// ```python + /// aaaaaaa[ + /// bbbbbbbb + /// ] = some_long_value + /// ``` + /// + /// This layout is preferred over [`RightToLeft`] if the left is unsplittable (single keyword like `return` or a Name) + /// because it has better performance characteristics. + LeftToRight { + /// The right side of an assignment or the value returned in a return statement. + value: &'a Expr, + + /// The parent statement that encloses the `value` expression. + statement: AnyNodeRef<'a>, + }, + + /// Prefers parenthesizing the value before splitting the left side. Specific to assignments. + /// + /// Formats what's left of `value` together with the assignment operator and the assigned `value`. + /// This layout prefers parenthesizing the value over parenthesizing the left (target or type annotation): + /// + /// ```python + /// aaaaaaa[bbbbbbbb] = some_long_value + /// ``` + /// + /// gets formatted to... + /// + /// ```python + /// aaaaaaa[bbbbbbbb] = ( + /// some_long_value + /// ) + /// ``` + /// + /// ... regardless whether the value will fit or not. + /// + /// The left only gets parenthesized if the left exceeds the configured line width on its own or + /// is forced to split because of a magical trailing comma or contains comments: + /// + /// ```python + /// aaaaaaa[bbbbbbbb_exceeds_the_line_width] = some_long_value + /// ``` + /// + /// gets formatted to + /// ```python + /// aaaaaaa[ + /// bbbbbbbb_exceeds_the_line_width + /// ] = some_long_value + /// ``` + /// + /// The layout avoids parenthesizing the value when the left splits to avoid + /// unnecessary parentheses. Adding the parentheses, as shown in the below example, reduces readability. + /// + /// ```python + /// aaaaaaa[ + /// bbbbbbbb_exceeds_the_line_width + /// ] = ( + /// some_long_value + /// ) + /// + /// ## Non-fluent Call Expressions + /// Non-fluent call expressions in the `value` position are only parenthesized if the opening parentheses + /// exceeds the configured line length. The layout prefers splitting after the opening parentheses + /// if the `callee` expression and the opening parentheses fit. + /// fits on the line. + RightToLeft { + /// The expression that comes before the assignment operator. This is either + /// the last target, or the type annotation of an annotated assignment. + before_operator: AnyBeforeOperator<'a>, + + /// The assignment operator. Either `Assign` (`=`) or the operator used by the augmented assignment statement. + operator: AnyAssignmentOperator, + + /// The assigned `value`. + value: &'a Expr, + + /// The assignment statement. + statement: AnyNodeRef<'a>, + }, } impl<'a> FormatStatementsLastExpression<'a> { - pub(super) fn new>>(expression: &'a Expr, parent: P) -> Self { - Self { - expression, - parent: parent.into(), + pub(super) fn left_to_right>>(value: &'a Expr, statement: S) -> Self { + Self::LeftToRight { + value, + statement: statement.into(), } } } 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, - }; + match self { + FormatStatementsLastExpression::LeftToRight { value, statement } => { + let can_inline_comment = should_inline_comments(value, *statement, f.context()); + + if !can_inline_comment { + return maybe_parenthesize_expression( + value, + *statement, + Parenthesize::IfBreaks, + ) + .fmt(f); + } - if !can_inline_comment { - return maybe_parenthesize_expression( - self.expression, - self.parent, - Parenthesize::IfBreaks, - ) - .fmt(f); - } + let comments = f.context().comments().clone(); + let expression_comments = comments.leading_dangling_trailing(*value); - let comments = f.context().comments().clone(); - let expression_comments = comments.leading_dangling_trailing(self.expression); + if let Some(inline_comments) = OptionalParenthesesInlinedComments::new( + &expression_comments, + *statement, + &comments, + ) { + let group_id = f.group_id("optional_parentheses"); - if expression_comments.has_leading() { - // Preserve the parentheses if the expression has any leading comments, - // same as `maybe_parenthesize_expression` - return self - .expression - .format() - .with_options(Parentheses::Always) - .fmt(f); - } + let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), 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); + best_fit_parenthesize(&format_with(|f| { + inline_comments.mark_formatted(); - let after_end_of_line = expression_comments - .trailing - .partition_point(|comment| comment.line_position().is_end_of_line()); + value.format().with_options(Parentheses::Never).fmt(f)?; - let (expression_inline_comments, expression_trailing_comments) = - expression_comments.trailing.split_at(after_end_of_line); + if !inline_comments.is_empty() { + // If the expressions exceeds the line width, format the comments in the parentheses + if_group_breaks(&inline_comments).fmt(f)?; + } - if expression_trailing_comments.is_empty() { - let inline_comments = OptionalParenthesesInlinedComments { - expression: expression_inline_comments, - statement: stmt_inline_comments, - }; + Ok(()) + })) + .with_group_id(Some(group_id)) + .fmt(f)?; - let group_id = f.group_id("optional_parentheses"); - let f = &mut WithNodeLevel::new(NodeLevel::Expression(Some(group_id)), f); + 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)?; + } - best_fit_parenthesize(&format_with(|f| { - inline_comments.mark_formatted(); + Ok(()) + } else { + // Preserve the parentheses if the expression has any leading or trailing comments, + // to avoid syntax errors, similar to `maybe_parenthesize_expression`. + value.format().with_options(Parentheses::Always).fmt(f) + } + } + FormatStatementsLastExpression::RightToLeft { + before_operator, + operator, + value, + statement, + } => { + let should_inline_comments = should_inline_comments(value, *statement, f.context()); + + // Use the normal `maybe_parenthesize_layout` for splittable `value`s. + if !should_inline_comments + && !should_non_inlineable_use_best_fit(value, *statement, f.context()) + { + return write!( + f, + [ + before_operator, + space(), + operator, + space(), + maybe_parenthesize_expression( + value, + *statement, + Parenthesize::IfBreaks + ) + ] + ); + } - self.expression - .format() - .with_options(Parentheses::Never) - .fmt(f)?; + let comments = f.context().comments().clone(); + let expression_comments = comments.leading_dangling_trailing(*value); + + // Don't inline comments for attribute and call expressions for black compatibility + let inline_comments = if should_inline_comments { + OptionalParenthesesInlinedComments::new( + &expression_comments, + *statement, + &comments, + ) + } else if expression_comments.has_leading() + || expression_comments.has_trailing_own_line() + { + None + } else { + 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, + [ + before_operator, + space(), + operator, + space(), + value.format().with_options(Parentheses::Always) + ] + ); + }; + + // Prevent inline comments to be formatted as part of the expression. + inline_comments.mark_formatted(); - 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 mut last_target = before_operator.memoized(); + + // Don't parenthesize the `value` if it is known that the target will break. + // This is mainly a performance optimisation that avoids unnecessary memoization + // and using the costly `BestFitting` layout if it is already known that only the last variant + // can ever fit because the left breaks. + if last_target.inspect(f)?.will_break() { + return write!( + f, + [ + last_target, + space(), + operator, + space(), + value.format().with_options(Parentheses::Never), + inline_comments + ] + ); } - Ok(()) - })) - .with_group_id(Some(group_id)) - .fmt(f)?; - - 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)?; + let format_value = value.format().with_options(Parentheses::Never).memoized(); + + // Tries to fit the `left` and the `value` on a single line: + // ```python + // a = b = c + // ``` + let format_flat = format_with(|f| { + write!( + f, + [ + last_target, + space(), + operator, + space(), + format_value, + inline_comments + ] + ) + }); + + // Don't break the last assignment target but parenthesize the value to see if it fits (break right first). + // + // ```python + // a["bbbbb"] = ( + // c + // ) + // ``` + let format_parenthesize_value = format_with(|f| { + write!( + f, + [ + last_target, + space(), + 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(), + operator, + space(), + format_value, + inline_comments + ] + ) + }); + + // For call expressions, prefer breaking after the call expression's opening parentheses + // over parenthesizing the entire call expression. + if value.is_call_expr() { + best_fitting![ + format_flat, + // Avoid parenthesizing the call expression if the `(` fit on the line + format_args![ + last_target, + space(), + 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) + } } - - Ok(()) - } else { - self.expression - .format() - .with_options(Parentheses::Always) - .fmt(f) } } } @@ -283,6 +584,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 +643,97 @@ 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("=")]) + } + } + } +} + +#[derive(Copy, Clone, Debug)] +pub(super) enum AnyBeforeOperator<'a> { + Expression(&'a Expr), + TypeParams(&'a TypeParams), +} + +impl Format> for AnyBeforeOperator<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + match self { + AnyBeforeOperator::Expression(expression) => { + // Preserve parentheses around targets with comments. + if f.context().comments().has_leading(*expression) + || f.context().comments().has_trailing(*expression) + { + expression + .format() + .with_options(Parentheses::Preserve) + .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(expression, f.context()) { + expression.format().with_options(Parentheses::Never).fmt(f) + } else { + parenthesize_if_expands(&expression.format().with_options(Parentheses::Never)) + .fmt(f) + } + } + // Never parenthesize type params + AnyBeforeOperator::TypeParams(type_params) => type_params.format().fmt(f), + } + } +} + +/// Returns `true` for unsplittable expressions for which comments should be inlined. +fn should_inline_comments( + expression: &Expr, + parent: AnyNodeRef, + context: &PyFormatContext, +) -> bool { + 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 + } + _ => false, + } +} + +/// Tests whether an expression that for which comments shouldn't be inlined should use the best fit layout +fn should_non_inlineable_use_best_fit( + expr: &Expr, + parent: AnyNodeRef, + context: &PyFormatContext, +) -> bool { + match expr { + Expr::Attribute(attribute) => { + attribute.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } + Expr::Call(call) => call.needs_parentheses(parent, context) == OptionalParentheses::BestFit, + _ => false, + } +} + +/// Returns `true` for targets that should not be parenthesized if they split because their expanded +/// layout comes with their own set of parentheses. +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 19202ecf0f982..003c10a32a8e1 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, AnyBeforeOperator, + 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(), + ) + { + FormatStatementsLastExpression::RightToLeft { + before_operator: AnyBeforeOperator::Expression(target), + operator: AnyAssignmentOperator::AugAssign(*op), + value, + statement: item.into(), + } + .fmt(f)?; + } else { + write!( + f, + [ + target.format(), + space(), + op.format(), + token("="), + space(), + FormatStatementsLastExpression::left_to_right(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/src/statement/stmt_return.rs b/crates/ruff_python_formatter/src/statement/stmt_return.rs index b0c5253e0fa38..4eaa3d92613ab 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_return.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_return.rs @@ -30,7 +30,10 @@ impl FormatNodeRule for FormatStmtReturn { Some(value) => { write!( f, - [space(), FormatStatementsLastExpression::new(value, item)] + [ + space(), + FormatStatementsLastExpression::left_to_right(value, item) + ] ) } None => Ok(()), diff --git a/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs b/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs index 6376236afb7d1..b6ae69b4430af 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_type_alias.rs @@ -3,7 +3,10 @@ use ruff_python_ast::StmtTypeAlias; use crate::comments::{SourceComment, SuppressionKind}; 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, AnyBeforeOperator, FormatStatementsLastExpression, +}; #[derive(Default)] pub struct FormatStmtTypeAlias; @@ -20,6 +23,16 @@ impl FormatNodeRule for FormatStmtTypeAlias { write!(f, [token("type"), space(), name.as_ref().format()])?; if let Some(type_params) = type_params { + if is_prefer_splitting_right_hand_side_of_assignments_enabled(f.context()) { + return FormatStatementsLastExpression::RightToLeft { + before_operator: AnyBeforeOperator::TypeParams(type_params), + operator: AnyAssignmentOperator::Assign, + value, + statement: item.into(), + } + .fmt(f); + }; + write!(f, [type_params.format()])?; } @@ -29,7 +42,7 @@ impl FormatNodeRule for FormatStmtTypeAlias { space(), token("="), space(), - FormatStatementsLastExpression::new(value, item) + FormatStatementsLastExpression::left_to_right(value, item) ] ) } 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 278a20e4a6650..bf6f676be094e 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 b3a03820fa4ca..a589aeffed92e 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 1cde924609d48..762af6aa16c7e 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 18b9fa9a06268..e799efc3145f6 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 4279cd03f29db..23c95ef3a2756 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 eedd65bade86d..eea51abf01ce3 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@preview.py.snap @@ -204,17 +204,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 @@ -228,12 +228,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 69490f3caf53c..191b6d141bb42 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 203d43b643802..b78a7687774c4 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,40 @@ 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] + +``` + + 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 0000000000000..6fca0005232fd --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@statement__assignment_split_value_first.py.snap @@ -0,0 +1,457 @@ +--- +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 +####### +# Unsplittable target and value + +# Only parenthesize the value if it makes it fit, otherwise avoid parentheses. +b = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +bbbbbbbbbbbbbbbb = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv + +# Avoid parenthesizing the value even if the target exceeds the configured width +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = bbb + + +############ +# Splittable targets + +# 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 + +# Augmented assignment +req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" +] += dst + +# Always parenthesize the value if it avoids splitting the target, regardless of the value's width. +_a: a[aaaa] = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +) + +##### +# Avoid parenthesizing the value if the expression right before the `=` splits to avoid an unnecessary pair of parentheses + +# The type annotation is guaranteed to split because it is too long. +_a: a[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target is too long +( + aaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target splits because of a magic trailing comma +( + a, + b, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +# The targets split because of a comment +( + # leading + a +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a + # trailing +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a, # nested + b +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +####### +# Multi targets + +# Black always parenthesizes the right if using multiple targets regardless if the parenthesized value exceeds the +# the configured line width or not +aaaa = bbbbbbbbbbbbbbbb = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee +) + +# Black does parenthesize the target if the target itself exceeds the line width and only parenthesizes +# the values if it makes it fit. +# The second target is too long to ever fit into the configured line width. +aaaa = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdddd +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +# Does also apply for other multi target assignments, as soon as a single target exceeds the configured +# width +aaaaaa = a["aaa"] = bbbbb[aa, bbb, cccc] = dddddddddd = eeeeee = ( + fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + +###################### +# Call expressions: +# For unsplittable targets: Parenthesize the call expression if it makes it fit. +# +# For splittable targets: +# Only parenthesize a call expression if the parens of the call don't fit on the same line +# as the target. Don't parenthesize the call expression if the target (or annotation) right before +# splits. + +# Don't parenthesize the function call if the left is unsplittable. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = a.b.function( + arg1, arg2, arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = ( + function() +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = ( + a.b.function(arg1, arg2, arg3) +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function() +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +####### Fluent call expressions +# Uses the regular `Multiline` layout where the entire `value` gets parenthesized +# if it doesn't fit on the line. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use = ( + function().b().c([1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3) +) + + +####### +# Test comment inlining +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) + + +# 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 +) + +# Splits the target but not the value because of the magic trailing comma. +a[ + aaaaaaa, + b, +] = ( + cccccccc # comment +) + +# Splits the second target because of the comment and the first target because of the trailing comma. +a[ + aaaaaaa, + b, +] = ( + # leading comment + b +) = ( + cccccccc # comment +) + + +######## +# Type Alias Statement +type A[str, int, number] = VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtin + +type A[VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtinthatExceedsTheWidth] = str + +``` + +## 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 +docstring-code-line-width = 88 +preview = Enabled +``` + +```python +####### +# Unsplittable target and value + +# Only parenthesize the value if it makes it fit, otherwise avoid parentheses. +b = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +bbbbbbbbbbbbbbbb = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvv +) + +# Avoid parenthesizing the value even if the target exceeds the configured width +bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = bbb + + +############ +# Splittable targets + +# 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 + +# Augmented assignment +req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"][ + "access_request" +] += dst + +# Always parenthesize the value if it avoids splitting the target, regardless of the value's width. +_a: a[aaaa] = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +) + +##### +# Avoid parenthesizing the value if the expression right before the `=` splits to avoid an unnecessary pair of parentheses + +# The type annotation is guaranteed to split because it is too long. +_a: a[ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv +] = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target is too long +( + aaaaaaaaaaa, + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvv + +# The target splits because of a magic trailing comma +( + a, + b, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +# The targets split because of a comment +( + # leading + a +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a + # trailing +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +( + a, # nested + b, +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvvvv + +####### +# Multi targets + +# Black always parenthesizes the right if using multiple targets regardless if the parenthesized value exceeds the +# the configured line width or not +aaaa = bbbbbbbbbbbbbbbb = ( + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee +) + +# Black does parenthesize the target if the target itself exceeds the line width and only parenthesizes +# the values if it makes it fit. +# The second target is too long to ever fit into the configured line width. +aaaa = ( + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbdddd +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbvvvvvvvvvvvvvvvvvee + +# Does also apply for other multi target assignments, as soon as a single target exceeds the configured +# width +aaaaaa = a["aaa"] = bbbbb[aa, bbb, cccc] = dddddddddd = eeeeee = ( + fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff +) = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + +###################### +# Call expressions: +# For unsplittable targets: Parenthesize the call expression if it makes it fit. +# +# For splittable targets: +# Only parenthesize a call expression if the parens of the call don't fit on the same line +# as the target. Don't parenthesize the call expression if the target (or annotation) right before +# splits. + +# Don't parenthesize the function call if the left is unsplittable. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = a.b.function( + arg1, arg2, arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function() +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = ( + a.b.function(arg1, arg2, arg3) +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function() +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3 +) +this_is_a_ridiculously_long_name_and_nobodyddddddddddddddddddddddddddddddd = function( + [1, 2, 3], + arg1, + [1, 2, 3], + arg2, + [1, 2, 3], + arg3, + dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, + eeeeeeeeeeeeee, +) + +####### Fluent call expressions +# Uses the regular `Multiline` layout where the entire `value` gets parenthesized +# if it doesn't fit on the line. +this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use = ( + function().b().c([1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3) +) + + +####### +# Test comment inlining +value.__dict__[key] = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) +value.__dict__.keye = ( + "test" # set some Thrift field to non-None in the struct aa bb cc dd ee +) + + +# 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 + +# Splits the target but not the value because of the magic trailing comma. +a[ + aaaaaaa, + b, +] = cccccccc # comment + +# Splits the second target because of the comment and the first target because of the trailing comma. +a[ + aaaaaaa, + b, +] = ( + # leading comment + b +) = cccccccc # comment + + +######## +# Type Alias Statement +type A[str, int, number] = ( + VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtin +) + +type A[ + VeryLongTypeNameThatShouldBreakFirstToTheRightBeforeSplitngtinthatExceedsTheWidth +] = str +``` + + +