Skip to content

Commit 1188ffc

Browse files
authored
Disallow newlines in format specifiers of single quoted f- or t-strings (#18708)
1 parent 23261a3 commit 1188ffc

17 files changed

+521
-513
lines changed

crates/ruff_python_formatter/resources/test/fixtures/black/cases/pep_701.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@
7474
f'{(abc:=10)}'
7575

7676
f"This is a really long string, but just make sure that you reflow fstrings {
77-
2+2:d
78-
}"
77+
2+2:d}"
7978
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
8079

8180
f"{2+2=}"

crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -278,23 +278,21 @@
278278

279279
# Combine conversion flags with format specifiers
280280
x = f"{x = !s
281-
:>0
282-
283-
}"
284-
# This is interesting. There can be a comment after the format specifier but only if it's
285-
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
286-
# We'll format is as trailing comments.
287-
x = f"{x !s
288-
:>0
289-
# comment 21
290-
}"
281+
:>0}"
291282

292283
x = f"{
293284
x!s:>{
294285
0
295286
# comment 21-2
296287
}}"
297288

289+
f"{1
290+
# comment 21-3
291+
:}"
292+
293+
f"{1 # comment 21-4
294+
:} a"
295+
298296

299297
x = f"""
300298
{ # comment 22
@@ -311,14 +309,14 @@
311309
"""
312310

313311
# Mix of various features.
314-
f"{ # comment 26
312+
f"""{ # comment 26
315313
foo # after foo
316314
:>{
317315
x # after x
318316
}
319317
# comment 27
320318
# comment 28
321-
} woah {x}"
319+
} woah {x}"""
322320

323321

324322
f"""{foo
@@ -332,8 +330,7 @@
332330
f"{
333331
# comment 31
334332
foo
335-
:>
336-
}"
333+
:>}"
337334

338335
# Assignment statement
339336

@@ -487,13 +484,11 @@
487484

488485
# This is not a multiline f-string even though it has a newline after the format specifier.
489486
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{
490-
a:.3f
491-
}moreeeeeeeeeeeeeeeeeetest" # comment
487+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
492488

493489
aaaaaaaaaaaaaaaaaa = (
494490
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
495-
a:.3f
496-
}moreeeeeeeeeeeeeeeeeetest" # comment
491+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
497492
)
498493

499494
# The newline is only considered when it's a tripled-quoted f-string.

crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -274,23 +274,21 @@
274274

275275
# Combine conversion flags with format specifiers
276276
x = t"{x = !s
277-
:>0
278-
279-
}"
280-
# This is interesting. There can be a comment after the format specifier but only if it's
281-
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
282-
# We'll format is as trailing comments.
283-
x = t"{x !s
284-
:>0
285-
# comment 21
286-
}"
277+
:>0}"
287278

288279
x = f"{
289280
x!s:>{
290281
0
291282
# comment 21-2
292283
}}"
293284

285+
f"{1
286+
# comment 21-3
287+
:}"
288+
289+
f"{1 # comment 21-4
290+
:} a"
291+
294292
x = t"""
295293
{ # comment 22
296294
x = :.0{y # comment 23
@@ -306,14 +304,14 @@
306304
"""
307305

308306
# Mix of various features.
309-
t"{ # comment 26
307+
t"""{ # comment 26
310308
foo # after foo
311309
:>{
312310
x # after x
313311
}
314312
# comment 27
315313
# comment 28
316-
} woah {x}"
314+
} woah {x}"""
317315

318316
# Assignment statement
319317

@@ -467,13 +465,11 @@
467465

468466
# This is not a multiline t-string even though it has a newline after the format specifier.
469467
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{
470-
a:.3f
471-
}moreeeeeeeeeeeeeeeeeetest" # comment
468+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
472469

473470
aaaaaaaaaaaaaaaaaa = (
474471
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
475-
a:.3f
476-
}moreeeeeeeeeeeeeeeeeetest" # comment
472+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
477473
)
478474

479475
# The newline is only considered when it's a tripled-quoted t-string.

crates/ruff_python_formatter/src/comments/placement.rs

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -323,27 +323,18 @@ fn handle_enclosed_comment<'a>(
323323
AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment),
324324
AnyNodeRef::InterpolatedElement(element) => {
325325
if let Some(preceding) = comment.preceding_node() {
326-
if comment.line_position().is_own_line() && element.format_spec.is_some() {
327-
return if comment.following_node().is_some() {
328-
// Own line comment before format specifier
329-
// ```py
330-
// aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
331-
// aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
332-
// # comment
333-
// :.3f} cccccccccc"""
334-
// ```
335-
CommentPlacement::trailing(preceding, comment)
336-
} else {
337-
// TODO: This can be removed once format specifiers with a newline are a syntax error.
338-
// This is to handle cases like:
339-
// ```py
340-
// x = f"{x !s
341-
// :>0
342-
// # comment 21
343-
// }"
344-
// ```
345-
CommentPlacement::trailing(element, comment)
346-
};
326+
// Own line comment before format specifier
327+
// ```py
328+
// aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
329+
// aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd
330+
// # comment
331+
// :.3f} cccccccccc"""
332+
// ```
333+
if comment.line_position().is_own_line()
334+
&& element.format_spec.is_some()
335+
&& comment.following_node().is_some()
336+
{
337+
return CommentPlacement::trailing(preceding, comment);
347338
}
348339
}
349340

crates/ruff_python_formatter/src/other/interpolated_string_element.rs

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ruff_python_ast::{
77
};
88
use ruff_text_size::{Ranged, TextSlice};
99

10-
use crate::comments::{dangling_open_parenthesis_comments, trailing_comments};
10+
use crate::comments::dangling_open_parenthesis_comments;
1111
use crate::context::{
1212
InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel,
1313
};
@@ -203,7 +203,7 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
203203
// # comment 27
204204
// :test}"
205205
// ```
206-
if comments.has_trailing_own_line(expression) {
206+
if comments.has_trailing(expression) {
207207
soft_line_break().fmt(f)?;
208208
}
209209

@@ -214,31 +214,6 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
214214
}
215215
}
216216

217-
// These trailing comments can only occur if the format specifier is
218-
// present. For example,
219-
//
220-
// ```python
221-
// f"{
222-
// x:.3f
223-
// # comment
224-
// }"
225-
// ```
226-
227-
// This can also be triggered outside of a format spec, at
228-
// least until https://github.com/astral-sh/ruff/issues/18632 is a syntax error
229-
// TODO(https://github.com/astral-sh/ruff/issues/18632) Remove this
230-
// and double check if it is still necessary for the triple quoted case
231-
// once this is a syntax error.
232-
// ```py
233-
// f"{
234-
// foo
235-
// :{x}
236-
// # comment 28
237-
// } woah {x}"
238-
// ```
239-
// Any other trailing comments are attached to the expression itself.
240-
trailing_comments(comments.trailing(self.element)).fmt(f)?;
241-
242217
if conversion.is_none() && format_spec.is_none() {
243218
bracket_spacing.fmt(f)?;
244219
}
@@ -258,15 +233,7 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
258233
let mut f = WithNodeLevel::new(NodeLevel::ParenthesizedExpression, f);
259234

260235
if self.context.is_multiline() {
261-
// TODO: The `or comments.has_trailing...` can be removed once newlines in format specs are a syntax error.
262-
// This is to support the following case:
263-
// ```py
264-
// x = f"{x !s
265-
// :>0
266-
// # comment 21
267-
// }"
268-
// ```
269-
if format_spec.is_none() || comments.has_trailing_own_line(self.element) {
236+
if format_spec.is_none() {
270237
group(&format_args![
271238
open_parenthesis_comments,
272239
soft_block_indent(&item)
@@ -276,6 +243,7 @@ impl Format<PyFormatContext<'_>> for FormatInterpolatedElement<'_> {
276243
// For strings ending with a format spec, don't add a newline between the end of the format spec
277244
// and closing curly brace because that is invalid syntax for single quoted strings and
278245
// the newline is preserved as part of the format spec for triple quoted strings.
246+
279247
group(&format_args![
280248
open_parenthesis_comments,
281249
indent(&format_args![soft_line_break(), item])

crates/ruff_python_formatter/tests/fixtures.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,12 @@ fn format_file(source: &str, options: &PyFormatOptions, input_path: &Path) -> St
324324

325325
(Cow::Owned(without_markers), content)
326326
} else {
327-
let printed = format_module_source(source, options.clone()).expect("Formatting to succeed");
327+
let printed = format_module_source(source, options.clone()).unwrap_or_else(|err| {
328+
panic!(
329+
"Formatting `{input_path} was expected to succeed but it failed: {err}",
330+
input_path = input_path.display()
331+
)
332+
});
328333
let formatted_code = printed.into_code();
329334

330335
ensure_stability_when_formatting_twice(&formatted_code, options, input_path);

crates/ruff_python_formatter/tests/snapshots/black_compatibility@cases__pep_701.py.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ x = f"a{2+2:=^{foo(x+y**2):something else}one more}b"
8181
f'{(abc:=10)}'
8282
8383
f"This is a really long string, but just make sure that you reflow fstrings {
84-
2+2:d
85-
}"
84+
2+2:d}"
8685
f"This is a really long string, but just make sure that you reflow fstrings correctly {2+2:d}"
8786
8887
f"{2+2=}"

0 commit comments

Comments
 (0)