Skip to content

Commit 41f0aad

Browse files
authored
Add FString support to binary like formatting
## Summary This is the last part of the string - binary like formatting. It adds support for handling fstrings the same as "regular" strings. ## Test Plan I added a test for both binary and comparison. Small improvements across several projects **This PR** | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76083 | 1789 | 1632 | | django | 0.99966 | 2760 | 58 | | **transformers** | 0.99929 | 2587 | 454 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99978 | 3496 | 2173 | | **warehouse** | 0.99825 | 648 | 22 | | **zulip** | 0.99950 | 1437 | 27 | **Base** | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76083 | 1789 | 1632 | | django | 0.99966 | 2760 | 58 | | transformers | 0.99928 | 2587 | 454 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99978 | 3496 | 2173 | | warehouse | 0.99824 | 648 | 22 | | zulip | 0.99948 | 1437 | 28 | <!-- How was it tested? -->
1 parent 05951dd commit 41f0aad

File tree

6 files changed

+100
-43
lines changed

6 files changed

+100
-43
lines changed

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

+9
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ def test():
8484
+ x
8585
)
8686

87+
(
88+
b + c + d +
89+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
90+
f"bbbbbb{z}bbbbbbbbbbbbbbbbbbbbbbb"
91+
"cccccccccccccccccccccccccc"
92+
% aaaaaaaaaaaa
93+
+ x
94+
)
95+
8796
(
8897
b < c > d <
8998
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

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

+9
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ def f():
131131
in caplog.messages
132132
)
133133

134+
(
135+
b < c > d <
136+
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
137+
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
138+
"cccccccccccccccccccccccccc"
139+
% aaaaaaaaaaaa
140+
> x
141+
)
142+
134143
c = (a >
135144
# test leading binary comment
136145
"a" "b" * b

crates/ruff_python_formatter/src/expression/binary_like.rs

+23-38
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,16 @@ use smallvec::SmallVec;
55

66
use ruff_formatter::write;
77
use ruff_python_ast::{
8-
BytesConstant, Constant, Expr, ExprAttribute, ExprBinOp, ExprCompare, ExprConstant,
9-
ExprUnaryOp, StringConstant, UnaryOp,
8+
Constant, Expr, ExprAttribute, ExprBinOp, ExprCompare, ExprConstant, ExprUnaryOp, UnaryOp,
109
};
1110

1211
use crate::comments::{leading_comments, trailing_comments, Comments, SourceComment};
13-
use crate::expression::expr_constant::ExprConstantLayout;
1412
use crate::expression::parentheses::{
1513
in_parentheses_only_group, in_parentheses_only_soft_line_break,
1614
in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized,
1715
write_in_parentheses_only_group_end_tag, write_in_parentheses_only_group_start_tag,
1816
};
19-
use crate::expression::string::StringLayout;
17+
use crate::expression::string::{AnyString, FormatString, StringLayout};
2018
use crate::expression::OperatorPrecedence;
2119
use crate::prelude::*;
2220

@@ -197,29 +195,12 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
197195
let mut string_operands = flat_binary
198196
.operands()
199197
.filter_map(|(index, operand)| {
200-
if let Expr::Constant(
201-
constant @ ExprConstant {
202-
value:
203-
Constant::Str(StringConstant {
204-
implicit_concatenated: true,
205-
..
206-
})
207-
| Constant::Bytes(BytesConstant {
208-
implicit_concatenated: true,
209-
..
210-
}),
211-
..
212-
},
213-
) = operand.expression()
214-
{
215-
if is_expression_parenthesized(constant.into(), source) {
216-
None
217-
} else {
218-
Some((index, constant, operand))
219-
}
220-
} else {
221-
None
222-
}
198+
AnyString::from_expression(operand.expression())
199+
.filter(|string| {
200+
string.is_implicit_concatenated()
201+
&& !is_expression_parenthesized(string.into(), source)
202+
})
203+
.map(|string| (index, string, operand))
223204
})
224205
.peekable();
225206

@@ -296,11 +277,11 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
296277
f,
297278
[
298279
operand.leading_binary_comments().map(leading_comments),
299-
string_constant
300-
.format()
301-
.with_options(ExprConstantLayout::String(
302-
StringLayout::ImplicitConcatenatedStringInBinaryLike,
303-
)),
280+
leading_comments(comments.leading(&string_constant)),
281+
FormatString::new(&string_constant).with_layout(
282+
StringLayout::ImplicitConcatenatedStringInBinaryLike,
283+
),
284+
trailing_comments(comments.trailing(&string_constant)),
304285
operand.trailing_binary_comments().map(trailing_comments),
305286
line_suffix_boundary(),
306287
]
@@ -311,12 +292,16 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
311292
// "a" "b" + c
312293
// ^^^^^^^-- format the first operand of a binary expression
313294
// ```
314-
string_constant
315-
.format()
316-
.with_options(ExprConstantLayout::String(
317-
StringLayout::ImplicitConcatenatedStringInBinaryLike,
318-
))
319-
.fmt(f)?;
295+
write!(
296+
f,
297+
[
298+
leading_comments(comments.leading(&string_constant)),
299+
FormatString::new(&string_constant).with_layout(
300+
StringLayout::ImplicitConcatenatedStringInBinaryLike
301+
),
302+
trailing_comments(comments.trailing(&string_constant)),
303+
]
304+
)?;
320305
}
321306

322307
// Write the right operator and start the group for the right side (if any)

crates/ruff_python_formatter/src/expression/string.rs

+29-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bitflags::bitflags;
44

55
use ruff_formatter::{format_args, write, FormatError, FormatOptions, TabWidth};
66
use ruff_python_ast::node::AnyNodeRef;
7-
use ruff_python_ast::{self as ast, ExprConstant, ExprFString};
7+
use ruff_python_ast::{self as ast, Constant, ExprConstant, ExprFString, ExpressionRef};
88
use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType};
99
use ruff_python_parser::{Mode, Tok};
1010
use ruff_source_file::Locator;
@@ -24,12 +24,26 @@ enum Quoting {
2424
Preserve,
2525
}
2626

27+
#[derive(Clone, Debug)]
2728
pub(super) enum AnyString<'a> {
2829
Constant(&'a ExprConstant),
2930
FString(&'a ExprFString),
3031
}
3132

3233
impl<'a> AnyString<'a> {
34+
pub(crate) fn from_expression(expression: &'a Expr) -> Option<AnyString<'a>> {
35+
match expression {
36+
Expr::Constant(
37+
constant @ ExprConstant {
38+
value: Constant::Str(_) | Constant::Bytes(_),
39+
..
40+
},
41+
) => Some(AnyString::Constant(constant)),
42+
Expr::FString(fstring) => Some(AnyString::FString(fstring)),
43+
_ => None,
44+
}
45+
}
46+
3347
fn quoting(&self, locator: &Locator) -> Quoting {
3448
match self {
3549
Self::Constant(_) => Quoting::CanChange,
@@ -50,7 +64,7 @@ impl<'a> AnyString<'a> {
5064
}
5165

5266
/// Returns `true` if the string is implicitly concatenated.
53-
fn implicit_concatenated(&self) -> bool {
67+
pub(super) fn is_implicit_concatenated(&self) -> bool {
5468
match self {
5569
Self::Constant(ExprConstant { value, .. }) => value.is_implicit_concatenated(),
5670
Self::FString(ExprFString {
@@ -79,6 +93,15 @@ impl<'a> From<&AnyString<'a>> for AnyNodeRef<'a> {
7993
}
8094
}
8195

96+
impl<'a> From<&AnyString<'a>> for ExpressionRef<'a> {
97+
fn from(value: &AnyString<'a>) -> Self {
98+
match value {
99+
AnyString::Constant(expr) => ExpressionRef::Constant(expr),
100+
AnyString::FString(expr) => ExpressionRef::FString(expr),
101+
}
102+
}
103+
}
104+
82105
pub(super) struct FormatString<'a> {
83106
string: &'a AnyString<'a>,
84107
layout: StringLayout,
@@ -97,7 +120,7 @@ pub enum StringLayout {
97120
}
98121

99122
impl<'a> FormatString<'a> {
100-
pub(super) fn new(string: &'a AnyString) -> Self {
123+
pub(super) fn new(string: &'a AnyString<'a>) -> Self {
101124
if let AnyString::Constant(constant) = string {
102125
debug_assert!(constant.value.is_str() || constant.value.is_bytes());
103126
}
@@ -117,7 +140,7 @@ impl<'a> Format<PyFormatContext<'_>> for FormatString<'a> {
117140
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
118141
match self.layout {
119142
StringLayout::Default => {
120-
if self.string.implicit_concatenated() {
143+
if self.string.is_implicit_concatenated() {
121144
in_parentheses_only_group(&FormatStringContinuation::new(self.string)).fmt(f)
122145
} else {
123146
FormatStringPart::new(
@@ -972,9 +995,10 @@ fn format_docstring_line(
972995

973996
#[cfg(test)]
974997
mod tests {
975-
use crate::expression::string::count_indentation_like_black;
976998
use ruff_formatter::TabWidth;
977999

1000+
use crate::expression::string::count_indentation_like_black;
1001+
9781002
#[test]
9791003
fn test_indentation_like_black() {
9801004
let tab_width = TabWidth::try_from(8).unwrap();

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

+15
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ self._assert_skipping(
9090
+ x
9191
)
9292
93+
(
94+
b + c + d +
95+
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
96+
f"bbbbbb{z}bbbbbbbbbbbbbbbbbbbbbbb"
97+
"cccccccccccccccccccccccccc"
98+
% aaaaaaaaaaaa
99+
+ x
100+
)
101+
93102
(
94103
b < c > d <
95104
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
@@ -266,6 +275,12 @@ self._assert_skipping(
266275
"cccccccccccccccccccccccccc" % aaaaaaaaaaaa + x
267276
)
268277
278+
(
279+
b + c + d + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
280+
f"bbbbbb{z}bbbbbbbbbbbbbbbbbbbbbbb"
281+
"cccccccccccccccccccccccccc" % aaaaaaaaaaaa + x
282+
)
283+
269284
(
270285
b < c > d < "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
271286
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"

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

+15
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,15 @@ assert (
137137
in caplog.messages
138138
)
139139
140+
(
141+
b < c > d <
142+
f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
143+
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
144+
"cccccccccccccccccccccccccc"
145+
% aaaaaaaaaaaa
146+
> x
147+
)
148+
140149
c = (a >
141150
# test leading binary comment
142151
"a" "b" * b
@@ -356,6 +365,12 @@ assert (
356365
"be silently ignored by the index" in caplog.messages
357366
)
358367
368+
(
369+
b < c > d < f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
370+
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
371+
"cccccccccccccccccccccccccc" % aaaaaaaaaaaa > x
372+
)
373+
359374
c = (
360375
a >
361376
# test leading binary comment

0 commit comments

Comments
 (0)