diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py index f70290c01d..6a0cbc1581 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py @@ -242,18 +242,22 @@ f"{ # comment 15 }" # 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" -# 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""" +# 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, 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 @@ -285,6 +289,13 @@ x = f"{x !s # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + + x = f""" { # comment 22 x = :.0{y # comment 23 @@ -309,6 +320,21 @@ f"{ # comment 26 # 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 diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py index f430faab9b..a0321e4eb3 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py @@ -240,18 +240,20 @@ t"{ # comment 15 }" # 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 @@ -283,6 +285,12 @@ x = t"{x !s # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + x = t""" { # comment 22 x = :.0{y # comment 23 diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 63075a7a0a..dd19db96b2 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -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(_) diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index b1b3a7941a..bddd6d84e0 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -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, @@ -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, @@ -153,7 +153,7 @@ impl InterpolatedStringState { pub(crate) fn can_contain_line_breaks(self) -> Option { match self { InterpolatedStringState::InsideInterpolatedElement(context) => { - Some(context.can_contain_line_breaks()) + Some(context.is_multiline()) } InterpolatedStringState::Outside => None, } diff --git a/crates/ruff_python_formatter/src/other/interpolated_string.rs b/crates/ruff_python_formatter/src/other/interpolated_string.rs index 7a0c8b3c1c..c146395587 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string.rs @@ -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) } } diff --git a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs index 19e243f86a..3111776cf8 100644 --- a/crates/ruff_python_formatter/src/other/interpolated_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use ruff_formatter::{Buffer, FormatOptions as _, RemoveSoftLinesBuffer, format_args, write}; use ruff_python_ast::{ AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement, - InterpolatedStringLiteralElement, StringFlags, + InterpolatedStringLiteralElement, }; use ruff_text_size::{Ranged, TextSlice}; @@ -78,52 +78,10 @@ impl Format> 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. pub(crate) struct FormatInterpolatedElement<'a> { element: &'a InterpolatedElement, - context: InterpolatedElementContext, + context: InterpolatedStringContext, } impl<'a> FormatInterpolatedElement<'a> { @@ -131,13 +89,7 @@ impl<'a> FormatInterpolatedElement<'a> { element: &'a InterpolatedElement, context: InterpolatedStringContext, ) -> Self { - Self { - element, - context: InterpolatedElementContext { - parent_context: context, - has_format_spec: element.format_spec.is_some(), - }, - } + Self { element, context } } } @@ -151,6 +103,8 @@ impl Format> for FormatInterpolatedElement<'_> { .. } = self.element; + let expression = &**expression; + if let Some(debug_text) = debug_text { token("{").fmt(f)?; @@ -179,7 +133,7 @@ impl Format> for FormatInterpolatedElement<'_> { f, [ NormalizedDebugText(&debug_text.leading), - verbatim_text(&**expression), + verbatim_text(expression), NormalizedDebugText(&debug_text.trailing), ] )?; @@ -202,6 +156,8 @@ impl Format> for FormatInterpolatedElement<'_> { let comments = f.context().comments().clone(); 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 // curly brace to avoid turning it into a literal curly with `{{`. // @@ -216,7 +172,7 @@ impl Format> for FormatInterpolatedElement<'_> { // added to maintain consistency. let bracket_spacing = 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) } else { space().fmt(f) @@ -241,30 +197,48 @@ impl Format> for FormatInterpolatedElement<'_> { } 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)?; for element in &format_spec.elements { - FormatInterpolatedStringElement::new( - element, - self.context.interpolated_string(), - ) - .fmt(f)?; + FormatInterpolatedStringElement::new(element, self.context).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() { bracket_spacing.fmt(f)?; } @@ -283,12 +257,31 @@ impl Format> for FormatInterpolatedElement<'_> { { let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f); - if self.context.can_contain_line_breaks() { - group(&format_args![ - open_parenthesis_comments, - soft_block_indent(&item) - ]) - .fmt(&mut f)?; + if self.context.is_multiline() { + // TODO: The `or comments.has_trailing...` can be removed once newlines in format specs are a syntax error. + // This is to support the following case: + // ```py + // x = f"{x !s + // :>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 { let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index 1e694a66ff..b7aa0605d7 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -50,7 +50,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { if let InterpolatedStringState::InsideInterpolatedElement(parent_context) = 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() { // 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 diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap index 1b074e6b45..e31f4723b9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_formatter/tests/fixtures.rs input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py -snapshot_kind: text --- ## Input @@ -170,7 +169,7 @@ rf"\{"a"}" x = """foo {{ {2 + 2}bar baz""" -@@ -28,74 +26,62 @@ +@@ -28,55 +26,48 @@ x = f"""foo {{ {2 + 2}bar {{ baz""" @@ -242,12 +241,7 @@ rf"\{"a"}" f"{2+2=}" f"{2+2 = }" - f"{ 2 + 2 = }" - --f"""foo { -- datetime.datetime.now():%Y -+f"""foo {datetime.datetime.now():%Y - %m +@@ -88,14 +79,10 @@ %d }""" @@ -264,7 +258,7 @@ rf"\{"a"}" ) f"`escape` only permitted in {{'html', 'latex', 'latex-math'}}, \ -@@ -105,8 +91,10 @@ +@@ -105,8 +92,10 @@ rf"\{{\}}" f""" @@ -277,7 +271,7 @@ rf"\{"a"}" """ value: str = f"""foo -@@ -124,13 +112,15 @@ +@@ -124,13 +113,15 @@ 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"""foo {datetime.datetime.now():%Y +f"""foo { + datetime.datetime.now():%Y %m %d }""" diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap index 913dcaf547..1d5f69f234 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap @@ -248,18 +248,22 @@ f"{ # comment 15 }" # 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" -# 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""" +# 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, 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 @@ -291,6 +295,13 @@ x = f"{x !s # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + + x = f""" { # comment 22 x = :.0{y # comment 23 @@ -315,6 +326,21 @@ f"{ # comment 26 # 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 @@ -1008,26 +1034,32 @@ f"{ # comment 15 }" # comment 19 # 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 { + 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" +} 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 -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment } cccccccccc""" -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment} cccccccccc""" # Conversion flags @@ -1047,6 +1079,13 @@ x = f"{ # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + + x = f""" { # comment 22 x = :.0{y # comment 23 @@ -1071,6 +1110,19 @@ f"{ # comment 26 # 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 @@ -1236,10 +1288,12 @@ aaaaaaaaaaaaaaaaaa = ( ) # The newline is only considered when it's a tripled-quoted f-string. -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment # Remove the parentheses here @@ -1804,26 +1858,32 @@ f"{ # comment 15 }" # comment 19 # 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 { + 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" +} 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 -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment } cccccccccc""" -aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment} cccccccccc""" # Conversion flags @@ -1843,6 +1903,13 @@ x = f"{ # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + + x = f""" { # comment 22 x = :.0{y # comment 23 @@ -1867,6 +1934,19 @@ f"{ # comment 26 # 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 @@ -2032,10 +2112,12 @@ aaaaaaaaaaaaaaaaaa = ( ) # The newline is only considered when it's a tripled-quoted f-string. -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment -aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = f"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment # Remove the parentheses here diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap index e916c1ee27..fef750a2a9 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -246,18 +246,20 @@ t"{ # comment 15 }" # 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 @@ -289,6 +291,12 @@ x = t"{x !s # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + x = t""" { # comment 22 x = :.0{y # comment 23 @@ -1004,26 +1012,28 @@ t"{ # comment 15 }" # 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" + +# 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" +} 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 # which is part of the format specifier -aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment } cccccccccc""" -aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f # comment} cccccccccc""" # Conversion flags @@ -1043,6 +1053,12 @@ x = t"{ # comment 21 }" +x = f"{ + x!s:>{ + 0 + # comment 21-2 + }}" + x = t""" { # comment 22 x = :.0{y # comment 23 @@ -1232,10 +1248,12 @@ aaaaaaaaaaaaaaaaaa = ( ) # 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 -aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f }moreeeeeeeeeeeeeeeeeetest""" # comment # Remove the parentheses here diff --git a/crates/ty_python_semantic/src/python_platform.rs b/crates/ty_python_semantic/src/python_platform.rs index 0822165322..573165f82e 100644 --- a/crates/ty_python_semantic/src/python_platform.rs +++ b/crates/ty_python_semantic/src/python_platform.rs @@ -1,12 +1,10 @@ use std::fmt::{Display, Formatter}; -use ruff_macros::RustDoc; - /// The target platform to assume when resolving types. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "serde", - derive(serde::Serialize, serde::Deserialize, RustDoc), + derive(serde::Serialize, serde::Deserialize, ruff_macros::RustDoc), serde(rename_all = "kebab-case") )] pub enum PythonPlatform {