Skip to content

Commit d00b986

Browse files
committed
Disallow newlines in format specifiers of sinlge quoted f- or t-strings
1 parent dd12dd9 commit d00b986

16 files changed

+438
-426
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: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,7 @@
282282

283283
# Combine conversion flags with format specifiers
284284
x = f"{x = ! s
285-
:>0
286-
287-
}"
285+
:>0}"
288286

289287
x = f"{
290288
x!s:>{
@@ -308,14 +306,14 @@
308306
"""
309307

310308
# Mix of various features.
311-
f"{ # comment 26
309+
f"""{ # comment 26
312310
foo # after foo
313311
:>{
314312
x # after x
315313
}
316314
# comment 27
317315
# comment 28
318-
} woah {x}"
316+
} woah {x}"""
319317

320318

321319
f"""{foo
@@ -329,8 +327,7 @@
329327
f"{
330328
# comment 31
331329
foo
332-
:>
333-
}"
330+
:>}"
334331

335332
# Assignment statement
336333

@@ -484,13 +481,11 @@
484481

485482
# This is not a multiline f-string even though it has a newline after the format specifier.
486483
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{
487-
a:.3f
488-
}moreeeeeeeeeeeeeeeeeetest" # comment
484+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
489485

490486
aaaaaaaaaaaaaaaaaa = (
491487
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
492-
a:.3f
493-
}moreeeeeeeeeeeeeeeeeetest" # comment
488+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
494489
)
495490

496491
# 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: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,7 @@
278278

279279
# Combine conversion flags with format specifiers
280280
x = t"{x = ! s
281-
:>0
282-
283-
}"
281+
:>0}"
284282
# This is interesting. There can be a comment after the format specifier but only if it's
285283
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
286284
# We'll format is as trailing comments.
@@ -466,13 +464,11 @@
466464

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

472469
aaaaaaaaaaaaaaaaaa = (
473470
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
474-
a:.3f
475-
}moreeeeeeeeeeeeeeeeeetest" # comment
471+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
476472
)
477473

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

crates/ruff_python_formatter/src/other/interpolated_string_element.rs

Lines changed: 1 addition & 26 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
};
@@ -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
}

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 of `{input_path} 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=}"

crates/ruff_python_formatter/tests/snapshots/format@expression__fstring.py.snap

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,7 @@ x = f"aaaaaaaaa { x = ! r }"
288288

289289
# Combine conversion flags with format specifiers
290290
x = f"{x = ! s
291-
:>0
292-
293-
}"
291+
:>0}"
294292
295293
x = f"{
296294
x!s:>{
@@ -314,14 +312,14 @@ x = f"""{"foo " + # comment 24
314312
"""
315313
316314
# Mix of various features.
317-
f"{ # comment 26
315+
f"""{ # comment 26
318316
foo # after foo
319317
:>{
320318
x # after x
321319
}
322320
# comment 27
323321
# comment 28
324-
} woah {x}"
322+
} woah {x}"""
325323
326324
327325
f"""{foo
@@ -335,8 +333,7 @@ f"""{foo
335333
f"{
336334
# comment 31
337335
foo
338-
:>
339-
}"
336+
:>}"
340337
341338
# Assignment statement
342339

@@ -490,13 +487,11 @@ aaaaa[aaaaaaaaaaa] = (
490487

491488
# This is not a multiline f-string even though it has a newline after the format specifier.
492489
aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeee{
493-
a:.3f
494-
}moreeeeeeeeeeeeeeeeeetest" # comment
490+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
495491

496492
aaaaaaaaaaaaaaaaaa = (
497493
f"testeeeeeeeeeeeeeeeeeeeeeeeee{
498-
a:.3f
499-
}moreeeeeeeeeeeeeeeeeetest" # comment
494+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
500495
)
501496

502497
# The newline is only considered when it's a tripled-quoted f-string.
@@ -1096,12 +1091,13 @@ x = f"""{
10961091
"""
10971092
10981093
# Mix of various features.
1099-
f"{ # comment 26
1094+
f"""{ # comment 26
11001095
foo:>{ # after foo
11011096
x # after x
1102-
# comment 27
1103-
# comment 28
1104-
}} woah {x}"
1097+
}
1098+
# comment 27
1099+
# comment 28
1100+
} woah {x}"""
11051101
11061102
11071103
f"""{
@@ -1916,12 +1912,13 @@ x = f"""{
19161912
"""
19171913
19181914
# Mix of various features.
1919-
f"{ # comment 26
1915+
f"""{ # comment 26
19201916
foo:>{ # after foo
19211917
x # after x
1922-
# comment 27
1923-
# comment 28
1924-
}} woah {x}"
1918+
}
1919+
# comment 27
1920+
# comment 28
1921+
} woah {x}"""
19251922
19261923
19271924
f"""{

crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,7 @@ x = t"aaaaaaaaa { x = ! r }"
284284

285285
# Combine conversion flags with format specifiers
286286
x = t"{x = ! s
287-
:>0
288-
289-
}"
287+
:>0}"
290288
# This is interesting. There can be a comment after the format specifier but only if it's
291289
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
292290
# We'll format is as trailing comments.
@@ -472,13 +470,11 @@ aaaaa[aaaaaaaaaaa] = (
472470

473471
# This is not a multiline t-string even though it has a newline after the format specifier.
474472
aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{
475-
a:.3f
476-
}moreeeeeeeeeeeeeeeeeetest" # comment
473+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
477474

478475
aaaaaaaaaaaaaaaaaa = (
479476
t"testeeeeeeeeeeeeeeeeeeeeeeeee{
480-
a:.3f
481-
}moreeeeeeeeeeeeeeeeeetest" # comment
477+
a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment
482478
)
483479

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

crates/ruff_python_parser/src/error.rs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,13 @@ pub enum InterpolatedStringErrorType {
6767

6868
impl std::fmt::Display for InterpolatedStringErrorType {
6969
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
70-
use InterpolatedStringErrorType::{
71-
InvalidConversionFlag, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace,
72-
UnterminatedString, UnterminatedTripleQuotedString,
73-
};
7470
match self {
75-
UnclosedLbrace => write!(f, "expecting '}}'"),
76-
InvalidConversionFlag => write!(f, "invalid conversion character"),
77-
SingleRbrace => write!(f, "single '}}' is not allowed"),
78-
UnterminatedString => write!(f, "unterminated string"),
79-
UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"),
80-
LambdaWithoutParentheses => {
71+
Self::UnclosedLbrace => write!(f, "expecting '}}'"),
72+
Self::InvalidConversionFlag => write!(f, "invalid conversion character"),
73+
Self::SingleRbrace => write!(f, "single '}}' is not allowed"),
74+
Self::UnterminatedString => write!(f, "unterminated string"),
75+
Self::UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"),
76+
Self::LambdaWithoutParentheses => {
8177
write!(f, "lambda expressions are not allowed without parentheses")
8278
}
8379
}
@@ -402,6 +398,8 @@ pub enum LexicalErrorType {
402398
LineContinuationError,
403399
/// An unexpected end of file was encountered.
404400
Eof,
401+
/// Newline inside of a format spec for a single quoted f- or t-string.
402+
NewlineInFormatSpec,
405403
/// An unexpected error occurred.
406404
OtherError(Box<str>),
407405
}
@@ -423,33 +421,39 @@ impl LexicalErrorType {
423421
impl std::fmt::Display for LexicalErrorType {
424422
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
425423
match self {
426-
LexicalErrorType::StringError => write!(f, "Got unexpected string"),
427-
LexicalErrorType::FStringError(error) => write!(f, "f-string: {error}"),
428-
LexicalErrorType::TStringError(error) => write!(f, "t-string: {error}"),
429-
LexicalErrorType::InvalidByteLiteral => {
424+
Self::StringError => write!(f, "Got unexpected string"),
425+
Self::FStringError(error) => write!(f, "f-string: {error}"),
426+
Self::TStringError(error) => write!(f, "t-string: {error}"),
427+
Self::InvalidByteLiteral => {
430428
write!(f, "bytes can only contain ASCII literal characters")
431429
}
432-
LexicalErrorType::UnicodeError => write!(f, "Got unexpected unicode"),
433-
LexicalErrorType::IndentationError => {
430+
Self::UnicodeError => write!(f, "Got unexpected unicode"),
431+
Self::IndentationError => {
434432
write!(f, "unindent does not match any outer indentation level")
435433
}
436-
LexicalErrorType::UnrecognizedToken { tok } => {
434+
Self::UnrecognizedToken { tok } => {
437435
write!(f, "Got unexpected token {tok}")
438436
}
439-
LexicalErrorType::LineContinuationError => {
437+
Self::LineContinuationError => {
440438
write!(f, "Expected a newline after line continuation character")
441439
}
442-
LexicalErrorType::Eof => write!(f, "unexpected EOF while parsing"),
443-
LexicalErrorType::OtherError(msg) => write!(f, "{msg}"),
444-
LexicalErrorType::UnclosedStringError => {
440+
Self::Eof => write!(f, "unexpected EOF while parsing"),
441+
Self::OtherError(msg) => write!(f, "{msg}"),
442+
Self::UnclosedStringError => {
445443
write!(f, "missing closing quote in string literal")
446444
}
447-
LexicalErrorType::MissingUnicodeLbrace => {
445+
Self::MissingUnicodeLbrace => {
448446
write!(f, "Missing `{{` in Unicode escape sequence")
449447
}
450-
LexicalErrorType::MissingUnicodeRbrace => {
448+
Self::MissingUnicodeRbrace => {
451449
write!(f, "Missing `}}` in Unicode escape sequence")
452450
}
451+
Self::NewlineInFormatSpec => {
452+
write!(
453+
f,
454+
"newlines are not allowed in specifiers for single quoted f- or t-strings"
455+
)
456+
}
453457
}
454458
}
455459
}

0 commit comments

Comments
 (0)