mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-23 13:05:58 +00:00
Fix f-string formatting in assignment statement (#14454)
## Summary fixes: #13813 This PR fixes a bug in the formatting assignment statement when the value is an f-string. This is resolved by using custom best fit layouts if the f-string is (a) not already a flat f-string (thus, cannot be multiline) and (b) is not a multiline string (thus, cannot be flattened). So, it is used in cases like the following: ```py aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ expression}moreeeeeeeeeeeeeeeee" ``` Which is (a) `FStringLayout::Multiline` and (b) not a multiline. There are various other examples in the PR diff along with additional explanation and context as code comments. ## Test Plan Add multiple test cases for various scenarios.
This commit is contained in:
parent
e4cefd9bf9
commit
f3dac27e9a
15 changed files with 2184 additions and 74 deletions
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<PyFormatContext<'_>> 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<FormatFString<'a>> {
|
||||
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],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Quote> 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
+)
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue