mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:13:08 +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
|
@ -1273,6 +1273,15 @@ impl FStringValue {
|
||||||
matches!(self.inner, FStringValueInner::Concatenated(_))
|
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.
|
/// Returns a slice of all the [`FStringPart`]s contained in this value.
|
||||||
pub fn as_slice(&self) -> &[FStringPart] {
|
pub fn as_slice(&self) -> &[FStringPart] {
|
||||||
match &self.inner {
|
match &self.inner {
|
||||||
|
|
|
@ -418,3 +418,9 @@ if True:
|
||||||
"permissions to manage this role, or else members of this role won't receive "
|
"permissions to manage this role, or else members of this role won't receive "
|
||||||
"a notification."
|
"a notification."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This f-string should be flattened
|
||||||
|
xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa {
|
||||||
|
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
|
||||||
|
yyyyyyyyyyyyyy + zzzzzzzzzzz
|
||||||
|
)
|
||||||
|
|
|
@ -313,6 +313,321 @@ f"{ # comment 26
|
||||||
# comment 28
|
# comment 28
|
||||||
} woah {x}"
|
} 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
|
# Indentation
|
||||||
|
|
||||||
# What should be the indentation?
|
# What should be the indentation?
|
||||||
|
|
|
@ -28,7 +28,7 @@ impl NeedsParentheses for ExprBinOp {
|
||||||
} else if let Ok(string) = StringLike::try_from(&*self.left) {
|
} else if let Ok(string) = StringLike::try_from(&*self.left) {
|
||||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||||
if !string.is_implicit_concatenated()
|
if !string.is_implicit_concatenated()
|
||||||
&& string.is_multiline(context.source())
|
&& string.is_multiline(context)
|
||||||
&& has_parentheses(&self.right, context).is_some()
|
&& has_parentheses(&self.right, context).is_some()
|
||||||
&& !context.comments().has_dangling(self)
|
&& !context.comments().has_dangling(self)
|
||||||
&& !context.comments().has(string)
|
&& !context.comments().has(string)
|
||||||
|
|
|
@ -40,7 +40,7 @@ impl NeedsParentheses for ExprBytesLiteral {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
} else if StringLike::Bytes(self).is_multiline(context.source()) {
|
} else if StringLike::Bytes(self).is_multiline(context) {
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
|
|
|
@ -29,7 +29,7 @@ impl NeedsParentheses for ExprCompare {
|
||||||
} else if let Ok(string) = StringLike::try_from(&*self.left) {
|
} else if let Ok(string) = StringLike::try_from(&*self.left) {
|
||||||
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
// Multiline strings are guaranteed to never fit, avoid adding unnecessary parentheses
|
||||||
if !string.is_implicit_concatenated()
|
if !string.is_implicit_concatenated()
|
||||||
&& string.is_multiline(context.source())
|
&& string.is_multiline(context)
|
||||||
&& !context.comments().has(string)
|
&& !context.comments().has(string)
|
||||||
&& self.comparators.first().is_some_and(|right| {
|
&& self.comparators.first().is_some_and(|right| {
|
||||||
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
has_parentheses(right, context).is_some() && !context.comments().has(right)
|
||||||
|
|
|
@ -4,10 +4,12 @@ use ruff_text_size::TextSlice;
|
||||||
use crate::expression::parentheses::{
|
use crate::expression::parentheses::{
|
||||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||||
};
|
};
|
||||||
use crate::other::f_string::FormatFString;
|
use crate::other::f_string::{FStringLayout, FormatFString};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::string::implicit::FormatImplicitConcatenatedStringFlat;
|
use crate::string::implicit::{
|
||||||
use crate::string::{implicit::FormatImplicitConcatenatedString, Quoting, StringLikeExtensions};
|
FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat,
|
||||||
|
};
|
||||||
|
use crate::string::{Quoting, StringLikeExtensions};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FormatExprFString;
|
pub struct FormatExprFString;
|
||||||
|
@ -45,26 +47,11 @@ impl NeedsParentheses for ExprFString {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
}
|
} else if StringLike::FString(self).is_multiline(context)
|
||||||
// TODO(dhruvmanila): Ideally what we want here is a new variant which
|
|| self.value.as_single().is_some_and(|f_string| {
|
||||||
// is something like:
|
FStringLayout::from_f_string(f_string, context.source()).is_multiline()
|
||||||
// - 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()) {
|
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl NeedsParentheses for ExprStringLiteral {
|
||||||
) -> OptionalParentheses {
|
) -> OptionalParentheses {
|
||||||
if self.value.is_implicit_concatenated() {
|
if self.value.is_implicit_concatenated() {
|
||||||
OptionalParentheses::Multiline
|
OptionalParentheses::Multiline
|
||||||
} else if StringLike::String(self).is_multiline(context.source()) {
|
} else if StringLike::String(self).is_multiline(context) {
|
||||||
OptionalParentheses::Never
|
OptionalParentheses::Never
|
||||||
} else {
|
} else {
|
||||||
OptionalParentheses::BestFit
|
OptionalParentheses::BestFit
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use ruff_formatter::{write, FormatContext};
|
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_python_trivia::{PythonWhitespace, SimpleTokenKind, SimpleTokenizer};
|
||||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
|
@ -137,6 +137,7 @@ fn is_single_argument_parenthesized(argument: &Expr, call_end: TextSize, source:
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the arguments can hug directly to the enclosing parentheses in the call, as
|
/// 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.
|
/// in Black's `hug_parens_with_braces_and_square_brackets` preview style behavior.
|
||||||
///
|
///
|
||||||
|
@ -223,7 +224,13 @@ fn is_huggable_string_argument(
|
||||||
arguments: &Arguments,
|
arguments: &Arguments,
|
||||||
context: &PyFormatContext,
|
context: &PyFormatContext,
|
||||||
) -> bool {
|
) -> 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;
|
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 {
|
pub(crate) const fn is_multiline(self) -> bool {
|
||||||
matches!(self, FStringLayout::Multiline)
|
matches!(self, FStringLayout::Multiline)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer};
|
use ruff_formatter::{format_args, write, FormatError, RemoveSoftLinesBuffer};
|
||||||
use ruff_python_ast::{
|
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;
|
use crate::builders::parenthesize_if_expands;
|
||||||
|
@ -8,6 +9,7 @@ use crate::comments::{
|
||||||
trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment,
|
trailing_comments, Comments, LeadingDanglingTrailingComments, SourceComment,
|
||||||
};
|
};
|
||||||
use crate::context::{NodeLevel, WithNodeLevel};
|
use crate::context::{NodeLevel, WithNodeLevel};
|
||||||
|
use crate::expression::expr_f_string::f_string_quoting;
|
||||||
use crate::expression::parentheses::{
|
use crate::expression::parentheses::{
|
||||||
is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses,
|
is_expression_parenthesized, optional_parentheses, NeedsParentheses, OptionalParentheses,
|
||||||
Parentheses, Parenthesize,
|
Parentheses, Parenthesize,
|
||||||
|
@ -16,12 +18,16 @@ use crate::expression::{
|
||||||
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
|
can_omit_optional_parentheses, has_own_parentheses, has_parentheses,
|
||||||
maybe_parenthesize_expression,
|
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::statement::trailing_semicolon;
|
||||||
use crate::string::implicit::{
|
use crate::string::implicit::{
|
||||||
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
FormatImplicitConcatenatedStringExpanded, FormatImplicitConcatenatedStringFlat,
|
||||||
ImplicitConcatenatedLayout,
|
ImplicitConcatenatedLayout,
|
||||||
};
|
};
|
||||||
|
use crate::string::StringLikeExtensions;
|
||||||
use crate::{has_skip_comment, prelude::*};
|
use crate::{has_skip_comment, prelude::*};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[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
|
/// 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.
|
/// 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> {
|
pub(super) enum FormatStatementsLastExpression<'a> {
|
||||||
/// Prefers to split what's left of `value` before splitting the value.
|
/// Prefers to split what's left of `value` before splitting the value.
|
||||||
///
|
///
|
||||||
|
@ -286,11 +293,18 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
match self {
|
match self {
|
||||||
FormatStatementsLastExpression::LeftToRight { value, statement } => {
|
FormatStatementsLastExpression::LeftToRight { value, statement } => {
|
||||||
let can_inline_comment = should_inline_comments(value, *statement, f.context());
|
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())
|
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(
|
return maybe_parenthesize_expression(
|
||||||
value,
|
value,
|
||||||
*statement,
|
*statement,
|
||||||
|
@ -436,6 +450,79 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
best_fitting![single_line, joined_parenthesized, implicit_expanded]
|
best_fitting![single_line, joined_parenthesized, implicit_expanded]
|
||||||
.with_mode(BestFittingMode::AllLines)
|
.with_mode(BestFittingMode::AllLines)
|
||||||
.fmt(f)?;
|
.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 {
|
} else {
|
||||||
best_fit_parenthesize(&format_once(|f| {
|
best_fit_parenthesize(&format_once(|f| {
|
||||||
inline_comments.mark_formatted();
|
inline_comments.mark_formatted();
|
||||||
|
@ -474,7 +561,11 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
statement,
|
statement,
|
||||||
} => {
|
} => {
|
||||||
let should_inline_comments = should_inline_comments(value, *statement, f.context());
|
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())
|
FormatImplicitConcatenatedStringFlat::new(string, f.context())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -482,6 +573,7 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
if !should_inline_comments
|
if !should_inline_comments
|
||||||
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
|
&& !should_non_inlineable_use_best_fit(value, *statement, f.context())
|
||||||
&& format_implicit_flat.is_none()
|
&& format_implicit_flat.is_none()
|
||||||
|
&& format_f_string.is_none()
|
||||||
{
|
{
|
||||||
return write!(
|
return write!(
|
||||||
f,
|
f,
|
||||||
|
@ -503,7 +595,10 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
let expression_comments = comments.leading_dangling_trailing(*value);
|
let expression_comments = comments.leading_dangling_trailing(*value);
|
||||||
|
|
||||||
// Don't inline comments for attribute and call expressions for black compatibility
|
// 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(
|
OptionalParenthesesInlinedComments::new(
|
||||||
&expression_comments,
|
&expression_comments,
|
||||||
*statement,
|
*statement,
|
||||||
|
@ -542,7 +637,8 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
// This is mainly a performance optimisation that avoids unnecessary memoization
|
// 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
|
// and using the costly `BestFitting` layout if it is already known that only the last variant
|
||||||
// can ever fit because the left breaks.
|
// 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!(
|
return write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
|
@ -568,6 +664,11 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
} else {
|
} else {
|
||||||
format_implicit_flat.fmt(f)
|
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 {
|
} else {
|
||||||
value.format().with_options(Parentheses::Never).fmt(f)
|
value.format().with_options(Parentheses::Never).fmt(f)
|
||||||
}
|
}
|
||||||
|
@ -805,6 +906,132 @@ impl Format<PyFormatContext<'_>> for FormatStatementsLastExpression<'_> {
|
||||||
.with_mode(BestFittingMode::AllLines)
|
.with_mode(BestFittingMode::AllLines)
|
||||||
.fmt(f)
|
.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 {
|
} else {
|
||||||
best_fitting![
|
best_fitting![
|
||||||
single_line,
|
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)]
|
#[derive(Debug, Default)]
|
||||||
struct OptionalParenthesesInlinedComments<'a> {
|
struct OptionalParenthesesInlinedComments<'a> {
|
||||||
expression: &'a [SourceComment],
|
expression: &'a [SourceComment],
|
||||||
|
|
|
@ -154,7 +154,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiline strings can never fit on a single line.
|
// 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;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,25 +187,6 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let StringLikePart::FString(fstring) = part {
|
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 context.options().target_version().supports_pep_701() {
|
||||||
if is_fstring_with_quoted_format_spec_and_debug(fstring, context) {
|
if is_fstring_with_quoted_format_spec_and_debug(fstring, context) {
|
||||||
if preserve_quotes_requirement
|
if preserve_quotes_requirement
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
use memchr::memchr2;
|
use memchr::memchr2;
|
||||||
|
|
||||||
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
pub(crate) use normalize::{normalize_string, NormalizedString, StringNormalizer};
|
||||||
use ruff_python_ast::str::Quote;
|
use ruff_python_ast::str::Quote;
|
||||||
|
use ruff_python_ast::StringLikePart;
|
||||||
use ruff_python_ast::{
|
use ruff_python_ast::{
|
||||||
self as ast,
|
self as ast,
|
||||||
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
|
str_prefix::{AnyStringPrefix, StringLiteralPrefix},
|
||||||
AnyStringFlags, StringFlags,
|
AnyStringFlags, StringFlags,
|
||||||
};
|
};
|
||||||
|
use ruff_source_file::LineRanges;
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::Ranged;
|
||||||
|
|
||||||
use crate::expression::expr_f_string::f_string_quoting;
|
use crate::expression::expr_f_string::f_string_quoting;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::preview::is_f_string_formatting_enabled;
|
||||||
use crate::QuoteStyle;
|
use crate::QuoteStyle;
|
||||||
|
|
||||||
pub(crate) mod docstring;
|
pub(crate) mod docstring;
|
||||||
|
@ -90,7 +92,7 @@ impl From<Quote> for QuoteStyle {
|
||||||
pub(crate) trait StringLikeExtensions {
|
pub(crate) trait StringLikeExtensions {
|
||||||
fn quoting(&self, source: &str) -> Quoting;
|
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<'_> {
|
impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
|
@ -101,15 +103,59 @@ impl StringLikeExtensions for ast::StringLike<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_multiline(&self, source: &str) -> bool {
|
fn is_multiline(&self, context: &PyFormatContext) -> bool {
|
||||||
match self {
|
self.parts().any(|part| match part {
|
||||||
Self::String(_) | Self::Bytes(_) => self.parts().any(|part| {
|
StringLikePart::String(_) | StringLikePart::Bytes(_) => {
|
||||||
part.flags().is_triple_quoted()
|
part.flags().is_triple_quoted()
|
||||||
&& memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some()
|
&& context.source().contains_line_break(part.range())
|
||||||
}),
|
|
||||||
Self::FString(fstring) => {
|
|
||||||
memchr2(b'\n', b'\r', source[fstring.range].as_bytes()).is_some()
|
|
||||||
}
|
}
|
||||||
}
|
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
|
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py
|
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py
|
||||||
snapshot_kind: text
|
|
||||||
---
|
---
|
||||||
## Input
|
## Input
|
||||||
```python
|
```python
|
||||||
|
@ -425,6 +424,12 @@ if True:
|
||||||
"permissions to manage this role, or else members of this role won't receive "
|
"permissions to manage this role, or else members of this role won't receive "
|
||||||
"a notification."
|
"a notification."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This f-string should be flattened
|
||||||
|
xxxxxxxxxxxxxxxx = f"aaaaaaaaaaaaaaaaaaaaa {
|
||||||
|
expression } bbbbbbbbbbbbbbbbbbbbbbbb" + (
|
||||||
|
yyyyyyyyyyyyyy + zzzzzzzzzzz
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
|
@ -897,4 +902,24 @@ if True:
|
||||||
"permissions to manage this role, or else members of this role won't receive "
|
"permissions to manage this role, or else members of this role won't receive "
|
||||||
"a notification."
|
"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