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

This commit is contained in:
Micha Reiser 2025-06-17 07:39:42 +02:00 committed by GitHub
parent 2b731d19b9
commit c22f809049
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 323 additions and 198 deletions

View file

@ -242,18 +242,22 @@ f"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # comment 20
# Single-quoted f-strings with a format specificer can be multiline # The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { # quoted f-string.
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 # The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`.
# trailing newline # or we risk altering the meaning of the f-string.
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
variable:.3f} ddddddddddddddd eeeeeeee""" :.3f} ddddddddddddddd eeeeeeee"""
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f
} ddddddddddddddd eeeeeeee"""
# But, we can break the ones which don't have a format specifier aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" # comment
:.3f} cccccccccc"""
# Throw in a random comment in it but surprise, this is not a comment but just a text # 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 # which is part of the format specifier
@ -285,6 +289,13 @@ x = f"{x !s
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = f""" x = f"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -309,6 +320,21 @@ f"{ # comment 26
# comment 28 # comment 28
} woah {x}" } 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 # Assignment statement
# Even though this f-string has multiline expression, thus allowing us to break it at the # Even though this f-string has multiline expression, thus allowing us to break it at the

View file

@ -240,18 +240,20 @@ t"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # 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 { 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 # The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`.
# trailing newline # or we risk altering the meaning of the f-string.
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
variable:.3f} ddddddddddddddd eeeeeeee""" :.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 # 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 # which is part of the format specifier
@ -283,6 +285,12 @@ x = t"{x !s
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = t""" x = t"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23

View file

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

View file

@ -7,7 +7,7 @@ use ruff_python_parser::Tokens;
use crate::PyFormatOptions; use crate::PyFormatOptions;
use crate::comments::Comments; use crate::comments::Comments;
use crate::other::interpolated_string_element::InterpolatedElementContext; use crate::other::interpolated_string::InterpolatedStringContext;
pub struct PyFormatContext<'a> { pub struct PyFormatContext<'a> {
options: PyFormatOptions, options: PyFormatOptions,
@ -143,7 +143,7 @@ pub(crate) enum InterpolatedStringState {
/// curly brace in `f"foo {x}"`. /// curly brace in `f"foo {x}"`.
/// ///
/// The containing `FStringContext` is the surrounding f-string context. /// The containing `FStringContext` is the surrounding f-string context.
InsideInterpolatedElement(InterpolatedElementContext), InsideInterpolatedElement(InterpolatedStringContext),
/// The formatter is outside an f-string. /// The formatter is outside an f-string.
#[default] #[default]
Outside, Outside,
@ -153,7 +153,7 @@ impl InterpolatedStringState {
pub(crate) fn can_contain_line_breaks(self) -> Option<bool> { pub(crate) fn can_contain_line_breaks(self) -> Option<bool> {
match self { match self {
InterpolatedStringState::InsideInterpolatedElement(context) => { InterpolatedStringState::InsideInterpolatedElement(context) => {
Some(context.can_contain_line_breaks()) Some(context.is_multiline())
} }
InterpolatedStringState::Outside => None, InterpolatedStringState::Outside => None,
} }

View file

@ -21,8 +21,8 @@ impl InterpolatedStringContext {
self.enclosing_flags self.enclosing_flags
} }
pub(crate) const fn layout(self) -> InterpolatedStringLayout { pub(crate) const fn is_multiline(self) -> bool {
self.layout matches!(self.layout, InterpolatedStringLayout::Multiline)
} }
} }

View file

@ -3,7 +3,7 @@ use std::borrow::Cow;
use ruff_formatter::{Buffer, FormatOptions as _, RemoveSoftLinesBuffer, format_args, write}; use ruff_formatter::{Buffer, FormatOptions as _, RemoveSoftLinesBuffer, format_args, write};
use ruff_python_ast::{ use ruff_python_ast::{
AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement, AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement,
InterpolatedStringLiteralElement, StringFlags, InterpolatedStringLiteralElement,
}; };
use ruff_text_size::{Ranged, TextSlice}; use ruff_text_size::{Ranged, TextSlice};
@ -78,52 +78,10 @@ impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
} }
} }
/// Context representing an f-string expression element.
#[derive(Clone, Copy, Debug)]
pub(crate) struct InterpolatedElementContext {
/// The context of the parent f-string containing this expression element.
parent_context: InterpolatedStringContext,
/// Indicates whether this expression element has format specifier or not.
has_format_spec: bool,
}
impl InterpolatedElementContext {
/// Returns the [`InterpolatedStringContext`] containing this expression element.
pub(crate) fn interpolated_string(self) -> InterpolatedStringContext {
self.parent_context
}
/// Returns `true` if the expression element can contain line breaks.
pub(crate) fn can_contain_line_breaks(self) -> bool {
self.parent_context.layout().is_multiline()
// For a triple-quoted f-string, the element can't be formatted into multiline if it
// has a format specifier because otherwise the newline would be treated as part of the
// format specifier.
//
// Given the following f-string:
// ```python
// f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee"""
// ```
//
// We can't format it as:
// ```python
// f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
// variable:.3f
// } ddddddddddddddd eeeeeeee"""
// ```
//
// Here, the format specifier string would become ".3f\n", which is not what we want.
// But, if the original source code already contained a newline, they'll be preserved.
//
// The Python version is irrelevant in this case.
&& !(self.parent_context.flags().is_triple_quoted() && self.has_format_spec)
}
}
/// Formats an f-string expression element. /// Formats an f-string expression element.
pub(crate) struct FormatInterpolatedElement<'a> { pub(crate) struct FormatInterpolatedElement<'a> {
element: &'a InterpolatedElement, element: &'a InterpolatedElement,
context: InterpolatedElementContext, context: InterpolatedStringContext,
} }
impl<'a> FormatInterpolatedElement<'a> { impl<'a> FormatInterpolatedElement<'a> {
@ -131,13 +89,7 @@ impl<'a> FormatInterpolatedElement<'a> {
element: &'a InterpolatedElement, element: &'a InterpolatedElement,
context: InterpolatedStringContext, context: InterpolatedStringContext,
) -> Self { ) -> Self {
Self { Self { element, context }
element,
context: InterpolatedElementContext {
parent_context: context,
has_format_spec: element.format_spec.is_some(),
},
}
} }
} }
@ -151,6 +103,8 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
.. ..
} = self.element; } = self.element;
let expression = &**expression;
if let Some(debug_text) = debug_text { if let Some(debug_text) = debug_text {
token("{").fmt(f)?; token("{").fmt(f)?;
@ -179,7 +133,7 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
f, f,
[ [
NormalizedDebugText(&debug_text.leading), NormalizedDebugText(&debug_text.leading),
verbatim_text(&**expression), verbatim_text(expression),
NormalizedDebugText(&debug_text.trailing), NormalizedDebugText(&debug_text.trailing),
] ]
)?; )?;
@ -202,6 +156,8 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
let comments = f.context().comments().clone(); let comments = f.context().comments().clone();
let dangling_item_comments = comments.dangling(self.element); let dangling_item_comments = comments.dangling(self.element);
let multiline = self.context.is_multiline();
// If an expression starts with a `{`, we need to add a space before the // If an expression starts with a `{`, we need to add a space before the
// curly brace to avoid turning it into a literal curly with `{{`. // curly brace to avoid turning it into a literal curly with `{{`.
// //
@ -216,7 +172,7 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
// added to maintain consistency. // added to maintain consistency.
let bracket_spacing = let bracket_spacing =
needs_bracket_spacing(expression, f.context()).then_some(format_with(|f| { needs_bracket_spacing(expression, f.context()).then_some(format_with(|f| {
if self.context.can_contain_line_breaks() { if multiline {
soft_line_break_or_space().fmt(f) soft_line_break_or_space().fmt(f)
} else { } else {
space().fmt(f) space().fmt(f)
@ -241,30 +197,48 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
} }
if let Some(format_spec) = format_spec.as_deref() { if let Some(format_spec) = format_spec.as_deref() {
// ```py
// f"{
// foo
// # comment 27
// :test}"
// ```
if comments.has_trailing_own_line(expression) {
soft_line_break().fmt(f)?;
}
token(":").fmt(f)?; token(":").fmt(f)?;
for element in &format_spec.elements { for element in &format_spec.elements {
FormatInterpolatedStringElement::new( FormatInterpolatedStringElement::new(element, self.context).fmt(f)?;
element,
self.context.interpolated_string(),
)
.fmt(f)?;
} }
// These trailing comments can only occur if the format specifier is
// present. For example,
//
// ```python
// f"{
// x:.3f
// # comment
// }"
// ```
//
// Any other trailing comments are attached to the expression itself.
trailing_comments(comments.trailing(self.element)).fmt(f)?;
} }
// These trailing comments can only occur if the format specifier is
// present. For example,
//
// ```python
// f"{
// x:.3f
// # comment
// }"
// ```
// This can also be triggered outside of a format spec, at
// least until https://github.com/astral-sh/ruff/issues/18632 is a syntax error
// TODO(https://github.com/astral-sh/ruff/issues/18632) Remove this
// and double check if it is still necessary for the triple quoted case
// once this is a syntax error.
// ```py
// f"{
// foo
// :{x}
// # comment 28
// } woah {x}"
// ```
// Any other trailing comments are attached to the expression itself.
trailing_comments(comments.trailing(self.element)).fmt(f)?;
if conversion.is_none() && format_spec.is_none() { if conversion.is_none() && format_spec.is_none() {
bracket_spacing.fmt(f)?; bracket_spacing.fmt(f)?;
} }
@ -283,12 +257,31 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
{ {
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
if self.context.can_contain_line_breaks() { if self.context.is_multiline() {
group(&format_args![ // TODO: The `or comments.has_trailing...` can be removed once newlines in format specs are a syntax error.
open_parenthesis_comments, // This is to support the following case:
soft_block_indent(&item) // ```py
]) // x = f"{x !s
.fmt(&mut f)?; // :>0
// # comment 21
// }"
// ```
if format_spec.is_none() || comments.has_trailing_own_line(self.element) {
group(&format_args![
open_parenthesis_comments,
soft_block_indent(&item)
])
.fmt(&mut f)?;
} else {
// For strings ending with a format spec, don't add a newline between the end of the format spec
// and closing curly brace because that is invalid syntax for single quoted strings and
// the newline is preserved as part of the format spec for triple quoted strings.
group(&format_args![
open_parenthesis_comments,
indent(&format_args![soft_line_break(), item])
])
.fmt(&mut f)?;
}
} else { } else {
let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); let mut buffer = RemoveSoftLinesBuffer::new(&mut *f);

View file

@ -50,7 +50,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
if let InterpolatedStringState::InsideInterpolatedElement(parent_context) = if let InterpolatedStringState::InsideInterpolatedElement(parent_context) =
self.context.interpolated_string_state() self.context.interpolated_string_state()
{ {
let parent_flags = parent_context.interpolated_string().flags(); let parent_flags = parent_context.flags();
if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() { if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() {
// This logic is even necessary when using preserve and the target python version doesn't support PEP701 because // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because
// we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes

View file

@ -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/black/cases/pep_701.py input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py
snapshot_kind: text
--- ---
## Input ## Input
@ -170,7 +169,7 @@ rf"\{"a"}"
x = """foo {{ {2 + 2}bar x = """foo {{ {2 + 2}bar
baz""" baz"""
@@ -28,74 +26,62 @@ @@ -28,55 +26,48 @@
x = f"""foo {{ {2 + 2}bar {{ baz""" x = f"""foo {{ {2 + 2}bar {{ baz"""
@ -242,12 +241,7 @@ rf"\{"a"}"
f"{2+2=}" f"{2+2=}"
f"{2+2 = }" f"{2+2 = }"
f"{ 2 + 2 = }" @@ -88,14 +79,10 @@
-f"""foo {
- datetime.datetime.now():%Y
+f"""foo {datetime.datetime.now():%Y
%m
%d %d
}""" }"""
@ -264,7 +258,7 @@ rf"\{"a"}"
) )
f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \ f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \
@@ -105,8 +91,10 @@ @@ -105,8 +92,10 @@
rf"\{{\}}" rf"\{{\}}"
f""" f"""
@ -277,7 +271,7 @@ rf"\{"a"}"
""" """
value: str = f"""foo value: str = f"""foo
@@ -124,13 +112,15 @@ @@ -124,13 +113,15 @@
f'{{\\"kind\\":\\"ConfigMap\\",\\"metadata\\":{{\\"annotations\\":{{}},\\"name\\":\\"cluster-info\\",\\"namespace\\":\\"amazon-cloudwatch\\"}}}}' f'{{\\"kind\\":\\"ConfigMap\\",\\"metadata\\":{{\\"annotations\\":{{}},\\"name\\":\\"cluster-info\\",\\"namespace\\":\\"amazon-cloudwatch\\"}}}}'
@ -378,7 +372,8 @@ f"{2+2=}"
f"{2+2 = }" f"{2+2 = }"
f"{ 2 + 2 = }" f"{ 2 + 2 = }"
f"""foo {datetime.datetime.now():%Y f"""foo {
datetime.datetime.now():%Y
%m %m
%d %d
}""" }"""

View file

@ -248,18 +248,22 @@ f"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # comment 20
# Single-quoted f-strings with a format specificer can be multiline # The specifier of an f-string must hug the closing `}` because a multiline format specifier is invalid syntax in a single
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { # quoted f-string.
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 # The same applies for triple quoted f-strings, except that we need to preserve the newline before the closing `}`.
# trailing newline # or we risk altering the meaning of the f-string.
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
variable:.3f} ddddddddddddddd eeeeeeee""" :.3f} ddddddddddddddd eeeeeeee"""
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f
} ddddddddddddddd eeeeeeee"""
# But, we can break the ones which don't have a format specifier aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" # comment
:.3f} cccccccccc"""
# Throw in a random comment in it but surprise, this is not a comment but just a text # 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 # which is part of the format specifier
@ -291,6 +295,13 @@ x = f"{x !s
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = f""" x = f"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -315,6 +326,21 @@ f"{ # comment 26
# comment 28 # comment 28
} woah {x}" } 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 # Assignment statement
# Even though this f-string has multiline expression, thus allowing us to break it at the # Even though this f-string has multiline expression, thus allowing us to break it at the
@ -1008,26 +1034,32 @@ f"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # comment 20
# Single-quoted f-strings with a format specificer can be multiline # 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 { 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 variable:.3f
} ddddddddddddddd eeeeeeee" } ddddddddddddddd eeeeeeee"""
# But, if it's triple-quoted then we can't or the format specificer will have a aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
# trailing newline aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" # comment
:.3f} cccccccccc"""
# But, we can break the ones which don't have a format specifier
f"""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 # 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 # which is part of the format specifier
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment # comment
} cccccccccc""" } cccccccccc"""
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment} cccccccccc""" # comment} cccccccccc"""
# Conversion flags # Conversion flags
@ -1047,6 +1079,13 @@ x = f"{
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = f""" x = f"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -1071,6 +1110,19 @@ f"{ # comment 26
# comment 28 # comment 28
} woah {x}" } 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 # Assignment statement
# Even though this f-string has multiline expression, thus allowing us to break it at the # Even though this f-string has multiline expression, thus allowing us to break it at the
@ -1236,10 +1288,12 @@ aaaaaaaaaaaaaaaaaa = (
) )
# The newline is only considered when it's a tripled-quoted f-string. # The newline is only considered when it's a tripled-quoted f-string.
aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
# Remove the parentheses here # Remove the parentheses here
@ -1804,26 +1858,32 @@ f"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # comment 20
# Single-quoted f-strings with a format specificer can be multiline # 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 { 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 variable:.3f
} ddddddddddddddd eeeeeeee" } ddddddddddddddd eeeeeeee"""
# But, if it's triple-quoted then we can't or the format specificer will have a aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
# trailing newline aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" # comment
:.3f} cccccccccc"""
# But, we can break the ones which don't have a format specifier
f"""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 # 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 # which is part of the format specifier
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment # comment
} cccccccccc""" } cccccccccc"""
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment} cccccccccc""" # comment} cccccccccc"""
# Conversion flags # Conversion flags
@ -1843,6 +1903,13 @@ x = f"{
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = f""" x = f"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -1867,6 +1934,19 @@ f"{ # comment 26
# comment 28 # comment 28
} woah {x}" } 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 # Assignment statement
# Even though this f-string has multiline expression, thus allowing us to break it at the # Even though this f-string has multiline expression, thus allowing us to break it at the
@ -2032,10 +2112,12 @@ aaaaaaaaaaaaaaaaaa = (
) )
# The newline is only considered when it's a tripled-quoted f-string. # The newline is only considered when it's a tripled-quoted f-string.
aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
# Remove the parentheses here # Remove the parentheses here

View file

@ -246,18 +246,20 @@ t"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # 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 { 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 # The same applies for triple quoted t-strings, except that we need to preserve the newline before the closing `}`.
# trailing newline # or we risk altering the meaning of the f-string.
t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable
variable:.3f} ddddddddddddddd eeeeeeee""" :.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 # 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 # which is part of the format specifier
@ -289,6 +291,12 @@ x = t"{x !s
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = t""" x = t"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -1004,26 +1012,28 @@ t"{ # comment 15
}" # comment 19 }" # comment 19
# comment 20 # 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 { 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 variable:.3f
} ddddddddddddddd eeeeeeee" } 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"""
# 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 # 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 # which is part of the format specifier
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment # comment
} cccccccccc""" } cccccccccc"""
aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
# comment} cccccccccc""" # comment} cccccccccc"""
# Conversion flags # Conversion flags
@ -1043,6 +1053,12 @@ x = t"{
# comment 21 # comment 21
}" }"
x = f"{
x!s:>{
0
# comment 21-2
}}"
x = t""" x = t"""
{ # comment 22 { # comment 22
x = :.0{y # comment 23 x = :.0{y # comment 23
@ -1232,10 +1248,12 @@ aaaaaaaaaaaaaaaaaa = (
) )
# The newline is only considered when it's a tripled-quoted t-string. # The newline is only considered when it's a tripled-quoted t-string.
aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{
a:.3f
}moreeeeeeeeeeeeeeeeeetest""" # comment }moreeeeeeeeeeeeeeeeeetest""" # comment
# Remove the parentheses here # Remove the parentheses here

View file

@ -1,12 +1,10 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use ruff_macros::RustDoc;
/// The target platform to assume when resolving types. /// The target platform to assume when resolving types.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr( #[cfg_attr(
feature = "serde", feature = "serde",
derive(serde::Serialize, serde::Deserialize, RustDoc), derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc),
serde(rename_all = "kebab-case") serde(rename_all = "kebab-case")
)] )]
pub enum PythonPlatform { pub enum PythonPlatform {