Skip to content

Hug closing } when f-string expression has a format specifier #18704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -242,18 +242,22 @@
}" # comment 19
# comment 20

# Single-quoted f-strings with a format specificer can be multiline
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"
# The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single
# quoted f-string.
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
:.3f} ddddddddddddddd eeeeeeee"

# The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`.
# or we risk altering the meaning of the f-string.
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
:.3f} ddddddddddddddd eeeeeeee"""
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f
} ddddddddddddddd eeeeeeee"""

# But, if it's triple-quoted then we can't or the format specificer will have a
# trailing newline
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"""

# But, we can break the ones which don't have a format specifier
f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb"""
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
# comment
:.3f} cccccccccc"""

# Throw in a random comment in it but surprise, this is not a comment but just a text
# which is part of the format specifier
Expand Down Expand Up @@ -289,6 +293,13 @@
# comment 21
}"

x = f"{
x!s:>{
0
# comment 21-2
}}"


x = f"""
{ # comment 22
x = :.0{y # comment 23
Expand All @@ -313,6 +324,21 @@
# comment 28
} woah {x}"


f"""{foo
:a{
a # comment 29
# comment 30
}
}"""

# Regression test for https://github.com/astral-sh/ruff/issues/18672
f"{
# comment 31
foo
:>
}"

# Assignment statement

# Even though this f-string has multiline expression, thus allowing us to break it at the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,18 +240,20 @@
}" # comment 19
# comment 20

# Single-quoted t-strings with a format specificer can be multiline
# The specifier of a t-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single
# quoted f-string.
t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"
variable
:.3f} ddddddddddddddd eeeeeeee"

# But, if it's triple-quoted then we can't or the format specificer will have a
# trailing newline
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
variable:.3f} ddddddddddddddd eeeeeeee"""
# The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`.
# or we risk altering the meaning of the f-string.
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
:.3f} ddddddddddddddd eeeeeeee"""
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
:.3f
} ddddddddddddddd eeeeeeee"""

# But, we can break the ones which don't have a format specifier
t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb"""

# Throw in a random comment in it but surprise, this is not a comment but just a text
# which is part of the format specifier
Expand Down Expand Up @@ -287,6 +289,12 @@
# comment 21
}"

x = f"{
x!s:>{
0
# comment 21-2
}}"

x = t"""
{ # comment 22
x = :.0{y # comment 23
Expand Down
47 changes: 26 additions & 21 deletions crates/ruff_python_formatter/src/comments/placement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,28 +321,33 @@ fn handle_enclosed_comment<'a>(
},
AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment),
AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment),
AnyNodeRef::InterpolatedElement(_) => {
// Handle comments after the format specifier (should be rare):
//
// ```python
// f"literal {
// expr:.3f
// # comment
// }"
// ```
//
// This is a valid comment placement.
if matches!(
comment.preceding_node(),
Some(
AnyNodeRef::InterpolatedElement(_)
| AnyNodeRef::InterpolatedStringLiteralElement(_)
)
) {
CommentPlacement::trailing(comment.enclosing_node(), comment)
} else {
handle_bracketed_end_of_line_comment(comment, source)
AnyNodeRef::InterpolatedElement(element) => {
if let Some(preceding) = comment.preceding_node() {
if comment.line_position().is_own_line() && element.format_spec.is_some() {
return if comment.following_node().is_some() {
// Own line comment before format specifier
// ```py
// aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
// aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
// # comment
// :.3f} cccccccccc"""
// ```
CommentPlacement::trailing(preceding, comment)
} else {
// TODO: This can be removed once format specifiers with a newline are a syntax error.
// This is to handle cases like:
// ```py
// x = f"{x !s
// :>0
// # comment 21
// }"
// ```
CommentPlacement::trailing(element, comment)
};
}
}

handle_bracketed_end_of_line_comment(comment, source)
}

AnyNodeRef::ExprList(_)
Expand Down
6 changes: 3 additions & 3 deletions crates/ruff_python_formatter/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use ruff_python_parser::Tokens;

use crate::PyFormatOptions;
use crate::comments::Comments;
use crate::other::interpolated_string_element::InterpolatedElementContext;
use crate::other::interpolated_string::InterpolatedStringContext;

pub struct PyFormatContext<'a> {
options: PyFormatOptions,
Expand Down Expand Up @@ -143,7 +143,7 @@ pub(crate) enum InterpolatedStringState {
/// curly brace in `f"foo {x}"`.
///
/// The containing `FStringContext` is the surrounding f-string context.
InsideInterpolatedElement(InterpolatedElementContext),
InsideInterpolatedElement(InterpolatedStringContext),
/// The formatter is outside an f-string.
#[default]
Outside,
Expand All @@ -153,7 +153,7 @@ impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self {
InterpolatedStringState::InsideInterpolatedElement(context) => {
Some(context.can_contain_line_breaks())
Some(context.is_multiline())
}
InterpolatedStringState::Outside => None,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ impl InterpolatedStringContext {
self.enclosing_flags
}

pub(crate) const fn layout(self) -> InterpolatedStringLayout {
self.layout
pub(crate) const fn is_multiline(self) -> bool {
matches!(self.layout, InterpolatedStringLayout::Multiline)
}
}

Expand Down
Loading
Loading