diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index b0eb7f3e8a..f210c08b70 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -1273,6 +1273,15 @@ impl FStringValue { matches!(self.inner, FStringValueInner::Concatenated(_)) } + /// Returns the single [`FString`] if the f-string isn't implicitly concatenated, [`None`] + /// otherwise. + pub fn as_single(&self) -> Option<&FString> { + match &self.inner { + FStringValueInner::Single(FStringPart::FString(fstring)) => Some(fstring), + _ => None, + } + } + /// Returns a slice of all the [`FStringPart`]s contained in this value. pub fn as_slice(&self) -> &[FStringPart] { match &self.inner { diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 83c6f0ff9f..818dca0bc7 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -418,3 +418,9 @@ if True: "permissions to manage this role, or else members of this role won't receive " "a notification." ) + +# This f-string should be flattened +xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index e61467cfea..9da1aa403e 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -313,6 +313,321 @@ f"{ # comment 26 # comment 28 } woah {x}" +# Assignment statement + +# Even though this f-string has multiline expression, thus allowing us to break it at the +# curly braces, the f-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the f-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The f-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this f-string. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the f-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following f-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = f"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = f"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the f-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the f-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following f-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated f-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two f-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = f"test{ + expression +}flat" f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = f"test{ + expression +}flat" f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single f-string instead. + +# Don't inline f-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline f-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline f-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline f-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted f-string. +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# F-strings in other positions + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert f"aaaaaaaaa{ + expression}bbbbbbbbbbbb", f"cccccccccc{ + expression}dddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{expression}dddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# F-strings as a single argument to a call expression to test whether it's huggable or not. +call(f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(f"{ + aaaaaa + + '''test + more''' +}") + # Indentation # What should be the indentation? diff --git a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs index 15b2b32814..dcd4721860 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bin_op.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bin_op.rs @@ -28,7 +28,7 @@ impl NeedsParentheses for ExprBinOp { } else if let Ok(string) = StringLike::try_from(&*self.left) { // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses if !string.is_implicit_concatenated() - && string.is_multiline(context.source()) + && string.is_multiline(context) && has_parentheses(&self.right, context).is_some() && !context.comments().has_dangling(self) && !context.comments().has(string) diff --git a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs index 92d2af2d1f..15c0a6e4bf 100644 --- a/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_bytes_literal.rs @@ -40,7 +40,7 @@ impl NeedsParentheses for ExprBytesLiteral { ) -> OptionalParentheses { if self.value.is_implicit_concatenated() { OptionalParentheses::Multiline - } else if StringLike::Bytes(self).is_multiline(context.source()) { + } else if StringLike::Bytes(self).is_multiline(context) { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/expression/expr_compare.rs b/crates/ruff_python_formatter/src/expression/expr_compare.rs index 13f072cde5..0b10ccaa31 100644 --- a/crates/ruff_python_formatter/src/expression/expr_compare.rs +++ b/crates/ruff_python_formatter/src/expression/expr_compare.rs @@ -29,7 +29,7 @@ impl NeedsParentheses for ExprCompare { } else if let Ok(string) = StringLike::try_from(&*self.left) { // Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses if !string.is_implicit_concatenated() - && string.is_multiline(context.source()) + && string.is_multiline(context) && !context.comments().has(string) && self.comparators.first().is_some_and(|right| { has_parentheses(right, context).is_some() && !context.comments().has(right) diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index dd67fd1bea..a003a31577 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -4,10 +4,12 @@ use ruff_text_size::TextSlice; use crate::expression::parentheses::{ in_parentheses_only_group, NeedsParentheses, OptionalParentheses, }; -use crate::other::f_string::FormatFString; +use crate::other::f_string::{FStringLayout, FormatFString}; use crate::prelude::*; -use crate::string::implicit::FormatImplicitConcatenatedStringFlat; -use crate::string::{implicit::FormatImplicitConcatenatedString, Quoting, StringLikeExtensions}; +use crate::string::implicit::{ + FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat, +}; +use crate::string::{Quoting, StringLikeExtensions}; #[derive(Default)] pub struct FormatExprFString; @@ -45,26 +47,11 @@ impl NeedsParentheses for ExprFString { ) -> OptionalParentheses { if self.value.is_implicit_concatenated() { OptionalParentheses::Multiline - } - // TODO(dhruvmanila): Ideally what we want here is a new variant which - // is something like: - // - If the expression fits by just adding the parentheses, then add them and - // avoid breaking the f-string expression. So, - // ``` - // xxxxxxxxx = ( - // f"aaaaaaaaaaaa { xxxxxxx + yyyyyyyy } bbbbbbbbbbbbb" - // ) - // ``` - // - But, if the expression is too long to fit even with parentheses, then - // don't add the parentheses and instead break the expression at `soft_line_break`. - // ``` - // xxxxxxxxx = f"aaaaaaaaaaaa { - // xxxxxxxxx + yyyyyyyyyy - // } bbbbbbbbbbbbb" - // ``` - // This isn't decided yet, refer to the relevant discussion: - // https://github.com/astral-sh/ruff/discussions/9785 - else if StringLike::FString(self).is_multiline(context.source()) { + } else if StringLike::FString(self).is_multiline(context) + || self.value.as_single().is_some_and(|f_string| { + FStringLayout::from_f_string(f_string, context.source()).is_multiline() + }) + { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs index 9813958de5..429e55bad0 100644 --- a/crates/ruff_python_formatter/src/expression/expr_string_literal.rs +++ b/crates/ruff_python_formatter/src/expression/expr_string_literal.rs @@ -53,7 +53,7 @@ impl NeedsParentheses for ExprStringLiteral { ) -> OptionalParentheses { if self.value.is_implicit_concatenated() { OptionalParentheses::Multiline - } else if StringLike::String(self).is_multiline(context.source()) { + } else if StringLike::String(self).is_multiline(context) { OptionalParentheses::Never } else { OptionalParentheses::BestFit diff --git a/crates/ruff_python_formatter/src/other/arguments.rs b/crates/ruff_python_formatter/src/other/arguments.rs index 6e50f1cf01..8dc079fce9 100644 --- a/crates/ruff_python_formatter/src/other/arguments.rs +++ b/crates/ruff_python_formatter/src/other/arguments.rs @@ -1,5 +1,5 @@ use ruff_formatter::{write, FormatContext}; -use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, StringLike}; +use ruff_python_ast::{ArgOrKeyword, Arguments, Expr, StringFlags, StringLike}; use ruff_python_trivia::{PythonWhitespace, SimpleTokenKind, SimpleTokenizer}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; @@ -137,6 +137,7 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source: false } + /// Returns `true` if the arguments can hug directly to the enclosing parentheses in the call, as /// in Black's `hug_parens_with_braces_and_square_brackets` preview style behavior. /// @@ -223,7 +224,13 @@ fn is_huggable_string_argument( arguments: &Arguments, context: &PyFormatContext, ) -> bool { - if string.is_implicit_concatenated() || !string.is_multiline(context.source()) { + if string.is_implicit_concatenated() + || !string.is_multiline(context) + || !string + .parts() + .next() + .is_some_and(|part| part.flags().is_triple_quoted()) + { return false; } diff --git a/crates/ruff_python_formatter/src/other/f_string.rs b/crates/ruff_python_formatter/src/other/f_string.rs index 409697dfad..067c4378d1 100644 --- a/crates/ruff_python_formatter/src/other/f_string.rs +++ b/crates/ruff_python_formatter/src/other/f_string.rs @@ -140,6 +140,10 @@ impl FStringLayout { } } + pub(crate) const fn is_flat(self) -> bool { + matches!(self, FStringLayout::Flat) + } + pub(crate) const fn is_multiline(self) -> bool { matches!(self, FStringLayout::Multiline) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 7ad90cd025..51e355d6cd 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,6 +1,7 @@ use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer}; use ruff_python_ast::{ - AnyNodeRef, Expr, ExprAttribute, ExprCall, Operator, StmtAssign, StringLike, TypeParams, + AnyNodeRef, Expr, ExprAttribute, ExprCall, FStringPart, Operator, StmtAssign, StringLike, + TypeParams, }; use crate::builders::parenthesize_if_expands; @@ -8,6 +9,7 @@ use crate::comments::{ trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment, }; use crate::context::{NodeLevel, WithNodeLevel}; +use crate::expression::expr_f_string::f_string_quoting; use crate::expression::parentheses::{ is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses, Parentheses, Parenthesize, @@ -16,12 +18,16 @@ use crate::expression::{ can_omit_optional_parentheses, has_own_parentheses, has_parentheses, maybe_parenthesize_expression, }; -use crate::preview::is_join_implicit_concatenated_string_enabled; +use crate::other::f_string::{FStringLayout, FormatFString}; +use crate::preview::{ + is_f_string_formatting_enabled, is_join_implicit_concatenated_string_enabled, +}; use crate::statement::trailing_semicolon; use crate::string::implicit::{ FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat, ImplicitConcatenatedLayout, }; +use crate::string::StringLikeExtensions; use crate::{has_skip_comment, prelude::*}; #[derive(Default)] @@ -183,6 +189,7 @@ impl Format> for FormatTargetWithEqualOperator<'_> { /// /// 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. +#[derive(Debug)] pub(super) enum FormatStatementsLastExpression<'a> { /// Prefers to split what's left of `value` before splitting the value. /// @@ -286,11 +293,18 @@ impl Format> for FormatStatementsLastExpression<'_> { match self { FormatStatementsLastExpression::LeftToRight { value, statement } => { let can_inline_comment = should_inline_comments(value, *statement, f.context()); - let format_implicit_flat = StringLike::try_from(*value).ok().and_then(|string| { + + let string_like = StringLike::try_from(*value).ok(); + let format_f_string = + string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); - if !can_inline_comment && format_implicit_flat.is_none() { + if !can_inline_comment + && format_implicit_flat.is_none() + && format_f_string.is_none() + { return maybe_parenthesize_expression( value, *statement, @@ -436,6 +450,79 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![single_line, joined_parenthesized, implicit_expanded] .with_mode(BestFittingMode::AllLines) .fmt(f)?; + } else if let Some(format_f_string) = format_f_string { + inline_comments.mark_formatted(); + + let f_string_flat = format_with(|f| { + let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); + + write!(buffer, [format_f_string]) + }) + .memoized(); + + // F-String containing an expression with a magic trailing comma, a comment, or a + // multiline debug expression should never be joined. Use the default layout. + // ```python + // aaaa = f"aaaa {[ + // 1, 2, + // ]} bbbb" + // ``` + if f_string_flat.inspect(f)?.will_break() { + inline_comments.mark_unformatted(); + + return write!( + f, + [maybe_parenthesize_expression( + value, + *statement, + Parenthesize::IfBreaks, + )] + ); + } + + // Considering the following example: + // ```python + // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + // expression}moreeeeeeeeeeeeeeeee" + // ``` + + // Flatten the f-string. + // ```python + // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" + // ``` + let single_line = + format_with(|f| write!(f, [f_string_flat, inline_comments])); + + // Parenthesize the f-string and flatten the f-string. + // ```python + // aaaaaaaaaaaaaaaaaa = ( + // f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" + // ) + // ``` + let joined_parenthesized = format_with(|f| { + group(&format_args![ + token("("), + soft_block_indent(&format_args![f_string_flat, inline_comments]), + token(")"), + ]) + .with_group_id(Some(group_id)) + .should_expand(true) + .fmt(f) + }); + + // Avoid flattening or parenthesizing the f-string, keep the original + // f-string formatting. + // ```python + // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + // expression + // }moreeeeeeeeeeeeeeeee" + // ``` + let format_f_string = + format_with(|f| write!(f, [format_f_string, inline_comments])); + + best_fitting![single_line, joined_parenthesized, format_f_string] + .with_mode(BestFittingMode::AllLines) + .fmt(f)?; } else { best_fit_parenthesize(&format_once(|f| { inline_comments.mark_formatted(); @@ -474,7 +561,11 @@ impl Format> for FormatStatementsLastExpression<'_> { statement, } => { let should_inline_comments = should_inline_comments(value, *statement, f.context()); - let format_implicit_flat = StringLike::try_from(*value).ok().and_then(|string| { + + let string_like = StringLike::try_from(*value).ok(); + let format_f_string = + string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); @@ -482,6 +573,7 @@ impl Format> for FormatStatementsLastExpression<'_> { if !should_inline_comments && !should_non_inlineable_use_best_fit(value, *statement, f.context()) && format_implicit_flat.is_none() + && format_f_string.is_none() { return write!( f, @@ -503,7 +595,10 @@ impl Format> for FormatStatementsLastExpression<'_> { 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 || format_implicit_flat.is_some() { + let inline_comments = if should_inline_comments + || format_implicit_flat.is_some() + || format_f_string.is_some() + { OptionalParenthesesInlinedComments::new( &expression_comments, *statement, @@ -542,7 +637,8 @@ impl Format> for FormatStatementsLastExpression<'_> { // 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 format_implicit_flat.is_none() && last_target_breaks { + if format_implicit_flat.is_none() && format_f_string.is_none() && last_target_breaks + { return write!( f, [ @@ -568,6 +664,11 @@ impl Format> for FormatStatementsLastExpression<'_> { } else { format_implicit_flat.fmt(f) } + } else if let Some(format_f_string) = format_f_string.as_ref() { + // Similar to above, remove any soft line breaks emitted by the f-string + // formatting. + let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); + write!(buffer, [format_f_string]) } else { value.format().with_options(Parentheses::Never).fmt(f) } @@ -805,6 +906,132 @@ impl Format> for FormatStatementsLastExpression<'_> { .with_mode(BestFittingMode::AllLines) .fmt(f) } + } else if let Some(format_f_string) = &format_f_string { + // F-String containing an expression with a magic trailing comma, a comment, or a + // multiline debug expression should never be joined. Use the default layout. + // + // ```python + // aaaa, bbbb = f"aaaa {[ + // 1, 2, + // ]} bbbb" + // ``` + if format_value.inspect(f)?.will_break() { + inline_comments.mark_unformatted(); + + return write!( + f, + [ + before_operator, + space(), + operator, + space(), + maybe_parenthesize_expression( + value, + *statement, + Parenthesize::IfBreaks + ) + ] + ); + } + + let format_f_string = + format_with(|f| write!(f, [format_f_string, inline_comments])).memoized(); + + // Considering the following initial source: + // + // ```python + // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + // f"aaaaaaaaaaaaaaaaaaa { + // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + // ) + // ``` + // + // Keep the target flat, and use the regular f-string formatting. + // + // ```python + // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc + // } ddddddddddddddddddd" + // ``` + let flat_target_regular_f_string = format_with(|f| { + write!( + f, + [last_target, space(), operator, space(), format_f_string] + ) + }); + + // Expand the parent and parenthesize the flattened f-string. + // + // ```python + // aaaaaaaaaaaa[ + // "bbbbbbbbbbbbbbbb" + // ] = ( + // f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + // ) + // ``` + let split_target_value_parenthesized_flat = format_with(|f| { + write!( + f, + [ + group(&last_target).should_expand(true), + space(), + operator, + space(), + token("("), + group(&soft_block_indent(&format_args![ + format_value, + inline_comments + ])) + .should_expand(true), + token(")") + ] + ) + }); + + // Expand the parent, and use the regular f-string formatting. + // + // ```python + // aaaaaaaaaaaa[ + // "bbbbbbbbbbbbbbbb" + // ] = f"aaaaaaaaaaaaaaaaaaa { + // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc + // } ddddddddddddddddddd" + // ``` + let split_target_regular_f_string = format_with(|f| { + write!( + f, + [ + group(&last_target).should_expand(true), + space(), + operator, + space(), + format_f_string, + ] + ) + }); + + // This is only a perf optimisation. No point in trying all the "flat-target" + // variants if we know that the last target must break. + if last_target_breaks { + best_fitting![ + split_target_flat_value, + split_target_value_parenthesized_flat, + split_target_regular_f_string, + ] + .with_mode(BestFittingMode::AllLines) + .fmt(f) + } else { + best_fitting![ + single_line, + flat_target_parenthesize_value, + flat_target_regular_f_string, + split_target_flat_value, + split_target_value_parenthesized_flat, + split_target_regular_f_string, + ] + .with_mode(BestFittingMode::AllLines) + .fmt(f) + } } else { best_fitting![ single_line, @@ -818,6 +1045,95 @@ impl Format> for FormatStatementsLastExpression<'_> { } } +/// Formats an f-string that is at the value position of an assignment statement. +/// +/// This is just a wrapper around [`FormatFString`] while considering a special case when the +/// f-string is at an assignment statement's value position. +/// +/// This is necessary to prevent an instability where an f-string contains a multiline expression +/// and the f-string fits on the line, but only when it's surrounded by parentheses. +/// +/// ```python +/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +/// expression}moreeeeeeeeeeeeeeeee" +/// ``` +/// +/// Without the special handling, this would get formatted to: +/// ```python +/// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +/// expression +/// }moreeeeeeeeeeeeeeeee" +/// ``` +/// +/// However, if the parentheses already existed in the source like: +/// ```python +/// aaaaaaaaaaaaaaaaaa = ( +/// f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" +/// ) +/// ``` +/// +/// Then, it would remain unformatted because it fits on the line. This means that even in the +/// first example, the f-string should be formatted by surrounding it with parentheses. +/// +/// One might ask why not just use the `BestFit` layout in this case. Consider the following +/// example in which the f-string doesn't fit on the line even when surrounded by parentheses: +/// ```python +/// xxxxxxx = f"{ +/// {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +/// }" +/// ``` +/// +/// The `BestFit` layout will format this as: +/// ```python +/// xxxxxxx = ( +/// f"{ +/// { +/// 'aaaaaaaaaaaaaaaaaaaaaaaaa', +/// 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', +/// 'cccccccccccccccccccccccccc', +/// } +/// }" +/// ) +/// ``` +/// +/// The reason for this is because (a) f-string already has a multiline expression thus it tries to +/// break the expression and (b) the `BestFit` layout doesn't considers the layout where the +/// multiline f-string isn't surrounded by parentheses. +fn format_f_string_assignment<'a>( + string: StringLike<'a>, + context: &PyFormatContext, +) -> Option> { + if !is_f_string_formatting_enabled(context) { + return None; + } + + let StringLike::FString(expr) = string else { + return None; + }; + + let [FStringPart::FString(f_string)] = expr.value.as_slice() else { + return None; + }; + + // If the f-string is flat, there are no breakpoints from which it can be made multiline. + // This is the case when the f-string has no expressions or if it does then the expressions + // are flat (no newlines). + if FStringLayout::from_f_string(f_string, context.source()).is_flat() { + return None; + } + + // This checks whether the f-string is multi-line and it can *never* be flattened. Thus, + // it's useless to try the flattened layout. + if string.is_multiline(context) { + return None; + } + + Some(FormatFString::new( + f_string, + f_string_quoting(expr, context.source()), + )) +} + #[derive(Debug, Default)] struct OptionalParenthesesInlinedComments<'a> { expression: &'a [SourceComment], diff --git a/crates/ruff_python_formatter/src/string/implicit.rs b/crates/ruff_python_formatter/src/string/implicit.rs index 10489e1e0a..3db5fc7f9c 100644 --- a/crates/ruff_python_formatter/src/string/implicit.rs +++ b/crates/ruff_python_formatter/src/string/implicit.rs @@ -154,7 +154,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { } // Multiline strings can never fit on a single line. - if !string.is_fstring() && string.is_multiline(context.source()) { + if string.is_multiline(context) { return None; } @@ -187,25 +187,6 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { } if let StringLikePart::FString(fstring) = part { - if fstring.elements.iter().any(|element| match element { - // Same as for other literals. Multiline literals can't fit on a single line. - FStringElement::Literal(literal) => { - context.source().contains_line_break(literal.range()) - } - FStringElement::Expression(expression) => { - if is_f_string_formatting_enabled(context) { - // Expressions containing comments can't be joined. - context.comments().contains_comments(expression.into()) - } else { - // Multiline f-string expressions can't be joined if the f-string formatting is disabled because - // the string gets inserted in verbatim preserving the newlines. - context.source().contains_line_break(expression.range()) - } - } - }) { - return None; - } - if context.options().target_version().supports_pep_701() { if is_fstring_with_quoted_format_spec_and_debug(fstring, context) { if preserve_quotes_requirement diff --git a/crates/ruff_python_formatter/src/string/mod.rs b/crates/ruff_python_formatter/src/string/mod.rs index eaff5efac2..cc6cc7766c 100644 --- a/crates/ruff_python_formatter/src/string/mod.rs +++ b/crates/ruff_python_formatter/src/string/mod.rs @@ -1,16 +1,18 @@ use memchr::memchr2; - pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer}; use ruff_python_ast::str::Quote; +use ruff_python_ast::StringLikePart; use ruff_python_ast::{ self as ast, str_prefix::{AnyStringPrefix, StringLiteralPrefix}, AnyStringFlags, StringFlags, }; +use ruff_source_file::LineRanges; use ruff_text_size::Ranged; use crate::expression::expr_f_string::f_string_quoting; use crate::prelude::*; +use crate::preview::is_f_string_formatting_enabled; use crate::QuoteStyle; pub(crate) mod docstring; @@ -90,7 +92,7 @@ impl From for QuoteStyle { pub(crate) trait StringLikeExtensions { fn quoting(&self, source: &str) -> Quoting; - fn is_multiline(&self, source: &str) -> bool; + fn is_multiline(&self, context: &PyFormatContext) -> bool; } impl StringLikeExtensions for ast::StringLike<'_> { @@ -101,15 +103,59 @@ impl StringLikeExtensions for ast::StringLike<'_> { } } - fn is_multiline(&self, source: &str) -> bool { - match self { - Self::String(_) | Self::Bytes(_) => self.parts().any(|part| { + fn is_multiline(&self, context: &PyFormatContext) -> bool { + self.parts().any(|part| match part { + StringLikePart::String(_) | StringLikePart::Bytes(_) => { part.flags().is_triple_quoted() - && memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some() - }), - Self::FString(fstring) => { - memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some() + && context.source().contains_line_break(part.range()) } - } + StringLikePart::FString(f_string) => { + fn contains_line_break_or_comments( + elements: &ast::FStringElements, + context: &PyFormatContext, + ) -> bool { + elements.iter().any(|element| match element { + ast::FStringElement::Literal(literal) => { + context.source().contains_line_break(literal.range()) + } + ast::FStringElement::Expression(expression) => { + if is_f_string_formatting_enabled(context) { + // Expressions containing comments can't be joined. + // + // Format specifiers needs to be checked as well. For example, the + // following should be considered multiline because the literal + // part of the format specifier contains a newline at the end + // (`.3f\n`): + // + // ```py + // x = f"hello {a + b + c + d:.3f + // } world" + // ``` + context.comments().contains_comments(expression.into()) + || expression.format_spec.as_deref().is_some_and(|spec| { + contains_line_break_or_comments(&spec.elements, context) + }) + || expression.debug_text.as_ref().is_some_and(|debug_text| { + memchr2(b'\n', b'\r', debug_text.leading.as_bytes()) + .is_some() + || memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()) + .is_some() + }) + } else { + // Multiline f-string expressions can't be joined if the f-string + // formatting is disabled because the string gets inserted in + // verbatim preserving the newlines. + // + // We don't need to check format specifiers or debug text here + // because the expression range already includes them. + context.source().contains_line_break(expression.range()) + } + } + }) + } + + contains_line_break_or_comments(&f_string.elements, context) + } + }) } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 82d426ac70..5f2072ff10 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py -snapshot_kind: text --- ## Input ```python @@ -425,6 +424,12 @@ if True: "permissions to manage this role, or else members of this role won't receive " "a notification." ) + +# This f-string should be flattened +xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) ``` ## Output @@ -897,4 +902,24 @@ if True: "permissions to manage this role, or else members of this role won't receive " "a notification." ) + +# This f-string should be flattened +xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (yyyyyyyyyyyyyy + zzzzzzzzzzz) +``` + + +## Preview changes +```diff +--- Stable ++++ Preview +@@ -468,5 +468,6 @@ + ) + + # This f-string should be flattened +-xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa { +- expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (yyyyyyyyyyyyyy + zzzzzzzzzzz) ++xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa {expression} bbbbbbbbbbbbbbbbbbbbbbbb" + ( ++ yyyyyyyyyyyyyy + zzzzzzzzzzz ++) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index e00cd01460..d466502233 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py -snapshot_kind: text --- ## Input ```python @@ -320,6 +319,321 @@ f"{ # comment 26 # comment 28 } woah {x}" +# Assignment statement + +# Even though this f-string has multiline expression, thus allowing us to break it at the +# curly braces, the f-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the f-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The f-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this f-string. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the f-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following f-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = f"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = f"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the f-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the f-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following f-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated f-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two f-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = f"test{ + expression +}flat" f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = f"test{ + expression +}flat" f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single f-string instead. + +# Don't inline f-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline f-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline f-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline f-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted f-string. +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# F-strings in other positions + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert f"aaaaaaaaa{ + expression}bbbbbbbbbbbb", f"cccccccccc{ + expression}dddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{expression}dddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# F-strings as a single argument to a call expression to test whether it's huggable or not. +call(f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(f"{ + aaaaaa + + '''test + more''' +}") + # Indentation # What should be the indentation? @@ -521,9 +835,9 @@ x = f"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" # But, in this case, we would split the expression itself because it exceeds the line # length limit so we need not add the extra space. -xxxxxxx = f"{ - {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} -}" +xxxxxxx = ( + f"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} }" +) # And, split the expression itself because it exceeds the line length. xxxxxxx = f"{ { @@ -698,6 +1012,356 @@ f"{ # comment 26 # comment 28 } woah {x}" +# Assignment statement + +# Even though this f-string has multiline expression, thus allowing us to break it at the +# curly braces, the f-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" +) + +# Same as above +xxxxxxx = ( + f"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} }" +) + +# Similar to the previous example, but the f-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = f"{ + { + 'aaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'cccccccccccccccccccccccccc', + } +}" + +# Same as above but with an inline comment. The f-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this f-string. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the f-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# The following f-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = f"aaaa { + [ + 1, + 2, + ] +} bbbb" +# right-to-left +aaaa, bbbb = f"aaaa { + [ + 1, + 2, + ] +} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Here, the f-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the f-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) + +# But, the following f-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc +} ddddddddddddddddddd" +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbb + + cccccccccccccccccccccc + + dddddddddddddddddddddddddddd +} ddddddddddddddddddd" + +# This is an implicitly concatenated f-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two f-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = ( + f"test{expression}flat" + f"can be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + +# Similar to the above example but this fits within the line length limit. +a = f"test{expression}flatcan be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single f-string instead. + +# Don't inline f-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +# Don't inline f-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +# Don't inline f-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +# This is not a multiline f-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted f-string. +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + # comment + ] +}moee" # comment +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + ] + }moee" + # comment +) + +# F-strings in other positions + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if f"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccc{expression}dddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{ + expression +}dddddddddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", ( + f"cccccccccccccccc{expression}dddddddddddddddd" +) + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + +assert ( + f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { + expression +} dddddddddddddddddddddddddd" + +assert ( + f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), ( + f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" +) + +# F-strings as a single argument to a call expression to test whether it's huggable or not. +call(f"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}") + +call( + f"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}" +) + +call( + f"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }" +) + +call( + f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""" +) + +call( + f"""aaaaaaaaaaaaaaaa bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""" +) + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment +}""") + +call( + f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call( + f"{ + aaaaaa + + '''test + more''' + }" +) + # Indentation # What should be the indentation? @@ -1064,6 +1728,320 @@ f"{ # comment 26 # comment 28 } woah {x}" +# Assignment statement + +# Even though this f-string has multiline expression, thus allowing us to break it at the +# curly braces, the f-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the f-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = f"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The f-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this f-string. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the f-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# The following f-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = f"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = f"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the f-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the f-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) + +# But, the following f-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" + +# This is an implicitly concatenated f-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two f-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = ( + f"test{ + expression +}flat" + f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + +# Similar to the above example but this fits within the line length limit. +a = ( + f"test{ + expression +}flat" + f"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single f-string instead. + +# Don't inline f-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +# Don't inline f-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment + +# Don't inline f-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +# This is not a multiline f-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +# The newline is only considered when it's a tripled-quoted f-string. +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# F-strings in other positions + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }": + pass + +if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert f"aaaaaaaaa{ + expression}bbbbbbbbbbbb", f"cccccccccc{ + expression}dddddddddd" + +assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert ( + f"aaaaaaaaa{expression}bbbbbbbbbbbb" +), f"cccccccccccccccc{expression}dddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ( + f"ccccccc{expression}dddddddddd" +) + +assert ( + f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), f"ccccccc{expression}dddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# F-strings as a single argument to a call expression to test whether it's huggable or not. +call( + f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}" +) + +call( + f"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}" +) + +call( + f"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}" +) + +call( + f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""" +) + +call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + f"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call( + f"{ + aaaaaa + + '''test + more''' +}" +) + # Indentation # What should be the indentation? @@ -1238,8 +2216,16 @@ f"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccc x = f"{ # comment 13 {'x': 1, 'y': 2} = }" # But, if there's a format specifier or a conversion flag then we don't need to add -@@ -141,7 +149,11 @@ - }" +@@ -136,12 +144,16 @@ + + # But, in this case, we would split the expression itself because it exceeds the line + # length limit so we need not add the extra space. +-xxxxxxx = f"{ +- {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} +-}" ++xxxxxxx = ( ++ f"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} }" ++) # And, split the expression itself because it exceeds the line length. xxxxxxx = f"{ - {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} @@ -1453,7 +2439,435 @@ f"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccc # comment 27 # comment 28 } woah {x}" -@@ -318,29 +330,29 @@ +@@ -314,29 +326,36 @@ + # Even though this f-string has multiline expression, thus allowing us to break it at the + # curly braces, the f-string fits on a single line if it's moved inside the parentheses. + # We should prefer doing that instead. +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeee" ++aaaaaaaaaaaaaaaaaa = ( ++ f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" ++) + + # Same as above +-xxxxxxx = f"{ +- {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +-}" ++xxxxxxx = ( ++ f"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} }" ++) + + # Similar to the previous example, but the f-string will exceed the line length limit, + # we shouldn't add any parentheses here. + xxxxxxx = f"{ +- {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} ++ { ++ 'aaaaaaaaaaaaaaaaaaaaaaaaa', ++ 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', ++ 'cccccccccccccccccccccccccc', ++ } + }" + + # Same as above but with an inline comment. The f-string should be formatted inside the + # parentheses and the comment should be part of the line inside the parentheses. +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeee" # comment ++aaaaaaaaaaaaaaaaaa = ( ++ f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment ++) + + # Similar to the previous example but this time parenthesizing doesn't work because it + # exceeds the line length. So, avoid parenthesizing this f-string. + aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeee" # comment loooooooong ++ expression ++}moreeeeeeeeeeeeeeeee" # comment loooooooong + + # Similar to the previous example but we start with the parenthesized layout. This should + # remove the parentheses and format the f-string on a single line. This shows that the +@@ -348,17 +367,24 @@ + # The following f-strings are going to break because of the trailing comma so we should + # avoid using the best fit layout and instead use the default layout. + # left-to-right +-aaaa = f"aaaa {[ +- 1, 2, +-]} bbbb" ++aaaa = f"aaaa { ++ [ ++ 1, ++ 2, ++ ] ++} bbbb" + # right-to-left +-aaaa, bbbb = f"aaaa {[ +- 1, 2, +-]} bbbb" ++aaaa, bbbb = f"aaaa { ++ [ ++ 1, ++ 2, ++ ] ++} bbbb" + + # Using the right-to-left assignment statement variant. +-aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeee" # comment ++aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = ( ++ f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment ++) + + # Here, the f-string layout is flat but it exceeds the line length limit. This shouldn't + # try the custom best fit layout because the f-string doesn't have any split points. +@@ -373,60 +399,66 @@ + + # But, the following f-string does have a split point because of the multiline expression. + aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { +- aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" ++ aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc ++} ddddddddddddddddddd" + aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { +- aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" ++ aaaaaaaaaaaaaaaaaaaa ++ + bbbbbbbbbbbbbbbbbbbbb ++ + cccccccccccccccccccccc ++ + dddddddddddddddddddddddddddd ++} ddddddddddddddddddd" + + # This is an implicitly concatenated f-string but it cannot be joined because otherwise + # it'll exceed the line length limit. So, the two f-strings will be inside parentheses + # instead and the inline comment should be outside the parentheses. + a = ( +- f"test{ +- expression +-}flat" +- f"can be { +- joined +-} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" ++ f"test{expression}flat" ++ f"can be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ) # inline + + # Similar to the above example but this fits within the line length limit. +-a = ( +- f"test{ +- expression +-}flat" +- f"can be { +- joined +-} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" +-) # inline ++a = f"test{expression}flatcan be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + # The following test cases are adopted from implicit string concatenation but for a + # single f-string instead. + + # Don't inline f-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma + aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +-[a,] ++ [ ++ a, ++ ] + }moreeeeeeeeeeeeeeeeeeee" # comment + + aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +-[a,] ++ [ ++ a, ++ ] + }moreeeeeeeeeeeeeeeeeeee" # comment + + aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +-[a,] ++ [ ++ a, ++ ] + }moreeeeeeeeeeeeeeeeeeee" # comment + + aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +-[a,] ++ [ ++ a, ++ ] + }moreeeeeeeeeeeeeeeeeeee" # comment + + # Don't inline f-strings that contain commented expressions +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ ++aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ ++ [ + a # comment +- ]}moreeeeeeeeeeeeeeeeeetest" # comment ++ ] ++}moreeeeeeeeeeeeeeeeeetest" # comment + +-aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{[ ++aaaaa[aaaaaaaaaaa] = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ ++ [ + a # comment +- ]}moreeeeeeeeeeeeeeeeeetest" # comment ++ ] ++}moreeeeeeeeeeeeeeeeeetest" # comment + + # Don't inline f-strings with multiline debug expressions or format specifiers + aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ +@@ -445,31 +477,37 @@ + =}moreeeeeeeeeeeeeeeeeetest" # comment + + # This is not a multiline f-string even though it has a newline after the format specifier. +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ +- a:.3f +- }moreeeeeeeeeeeeeeeeeetest" # comment ++aaaaaaaaaaaaaaaaaa = ( ++ f"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ++) + +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{ +- a:.3f +- }moreeeeeeeeeeeeeeeeeetest" # comment ++aaaaaaaaaaaaaaaaaa = ( ++ f"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment ++) + + # The newline is only considered when it's a tripled-quoted f-string. +-aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ +- a:.3f ++aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +-aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ +- a:.3f ++aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + + # Remove the parentheses here +-aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, +- # comment +- ]}moee" # comment ++aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ ++ [ ++ a, ++ b, ++ # comment ++ ] ++}moee" # comment + # ... but not here because of the ownline comment + aaaaaaaaaaaaaaaaaa = ( +- f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, +- ]}moee" ++ f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ ++ [ ++ a, ++ b, ++ ] ++ }moee" + # comment + ) + +@@ -481,13 +519,11 @@ + pass + + if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- }": ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++}": + pass + +-if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-}": ++if f"aaaaaaaaaaa {ttttteeeeeeeeest} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + + if f"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment +@@ -495,24 +531,33 @@ + }": + pass + +-if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +-}": ++if f"aaaaaaaaaaa { ++ [ ++ ttttteeeeeeeeest, ++ ] ++} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +-if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { +- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +- }": ++if f"aaaaaaaaaaa { ++ [ ++ ttttteeeeeeeeest, ++ ] ++} more { ++ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ++}": + pass + +-if f"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { ++if f"aaaaaaaaaaa { ++ [ ++ ttttteeeeeeeeest, ++ ] ++} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }": + pass + + # For loops +-for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeeeeee": ++for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + + for a in ( +@@ -521,7 +566,8 @@ + pass + + for a in f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": ++ expression ++}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + + for a in ( +@@ -530,8 +576,7 @@ + pass + + # With statements +-with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeeeeee": ++with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + + with ( +@@ -540,7 +585,8 @@ + pass + + with f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +- expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": ++ expression ++}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + + with ( +@@ -549,78 +595,80 @@ + pass + + # Assert statements +-assert f"aaaaaaaaa{ +- expression}bbbbbbbbbbbb", f"cccccccccc{ +- expression}dddddddddd" ++assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccc{expression}dddddddddd" + + assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", f"cccccccccccccccc{ +- expression}dddddddddddddddd" ++ expression ++}dddddddddddddddd" + +-assert ( +- f"aaaaaaaaa{expression}bbbbbbbbbbbb" +-), f"cccccccccccccccc{expression}dddddddddddddddd" ++assert f"aaaaaaaaa{expression}bbbbbbbbbbbb", ( ++ f"cccccccccccccccc{expression}dddddddddddddddd" ++) + + assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ +- expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", ( +- f"ccccccc{expression}dddddddddd" +-) ++ expression ++}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccc{expression}dddddddddd" + + assert ( + f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ), f"ccccccc{expression}dddddddddd" + + assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ +- expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { +- expression} dddddddddddddddddddddddddd" ++ expression ++}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"ccccccccccccccccccccc { ++ expression ++} dddddddddddddddddddddddddd" + +-assert f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" ++assert ( ++ f"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ++), ( ++ f"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" ++) + + # F-strings as a single argument to a call expression to test whether it's huggable or not. +-call( +- f"{ +- testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +-}" +-) ++call(f"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}") + + call( +- f"{ +- testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +-}" ++ f"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}" + ) + + call( + f"{ # comment +- testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +-}" ++ testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ++ }" + ) + + call( + f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""" + ) + +-call(f"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +- }""") ++call( ++ f"""aaaaaaaaaaaaaaaa bbbbbbbbbb { ++ testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee ++ }""" ++) + + call(f"""aaaaaaaaaaaaaaaa +- bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +- }""") ++ bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + + call(f"""aaaaaaaaaaaaaaaa +- bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment +- }""") ++ bbbbbbbbbb { ++ testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment ++}""") + + call( + f"""aaaaaaaaaaaaaaaa +- bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment +- }""" ++ bbbbbbbbbb { ++ testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment ++ }""" + ) + + call( + f"{ +- aaaaaa +- + '''test ++ aaaaaa ++ + '''test + more''' +-}" ++ }" + ) + + # Indentation +@@ -632,29 +680,29 @@ if indent2: foo = f"""hello world hello {