Skip to content

Commit 6b290c0

Browse files
committed
Fix unstable with-items formatting
1 parent 461cdad commit 6b290c0

File tree

10 files changed

+367
-250
lines changed

10 files changed

+367
-250
lines changed

crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,45 +13,40 @@
1313
pass
1414
# trailing
1515

16-
1716
with (
18-
a # a
19-
, # comma
20-
b # c
21-
): # colon
17+
a # a
18+
, # comma
19+
b # c
20+
): # colon
2221
pass
2322

24-
2523
with (
26-
a # a
27-
as # as
28-
# own line
29-
b # b
30-
, # comma
31-
c # c
32-
): # colon
24+
a # a
25+
as # as
26+
# own line
27+
b # b
28+
, # comma
29+
c # c
30+
): # colon
3331
pass # body
3432
# body trailing own
3533

3634
with (
37-
a # a
38-
as # as
39-
# own line
40-
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # b
35+
a # a
36+
as # as
37+
# own line
38+
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb # b
4139
): pass
4240

43-
44-
with (a,): # magic trailing comma
41+
with (a, ): # magic trailing comma
4542
pass
4643

47-
4844
with (a): # should remove brackets
4945
pass
5046

5147
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
5248
pass
5349

54-
5550
# currently unparsable by black: https://github.com/psf/black/issues/3678
5651
with (name_2 for name_0 in name_4):
5752
pass
@@ -87,21 +82,21 @@
8782
): pass
8883

8984
with (
90-
a # trailing same line comment
85+
a # trailing same line comment
9186
# trailing own line comment
9287
as b
9388
): pass
9489

95-
with (a # trailing same line comment
90+
with (a # trailing same line comment
9691
# trailing own line comment
9792
) as b: pass
9893

9994
with (
10095
(a
101-
# trailing own line comment
96+
# trailing own line comment
10297
)
103-
as # trailing as same line comment
104-
b # trailing b same line comment
98+
as # trailing as same line comment
99+
b # trailing b same line comment
105100
): pass
106101

107102
with (
@@ -304,6 +299,9 @@
304299
with anyio.CancelScope(shield=True) if get_running_loop() else contextlib.nullcontext():
305300
pass
306301

302+
if True:
303+
with anyio.CancelScope(shield=True) if get_running_loop() else contextlib.nullcontext() as c:
304+
pass
307305

308306
with Child(aaaaaaaaa, bbbbbbbbbbbbbbb, cccccc), Document(aaaaa, bbbbbbbbbb, ddddddddddddd):
309307
pass

crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/with_39.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
):
77
pass
88

9-
109
# Black avoids parenthesizing the with because it can make all with items fit by just breaking
1110
# around parentheses. We don't implement this optimisation because it makes it difficult to see where
1211
# the different context managers start and end.
@@ -37,7 +36,6 @@
3736
):
3837
pass
3938

40-
4139
# Black avoids parentheses here because it can make the entire with
4240
# header fit without requiring parentheses to do so.
4341
# We don't implement this optimisation because it very difficult to see where
@@ -66,7 +64,6 @@
6664
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
6765
pass
6866

69-
7067
# Black parenthesizes this binary expression but also preserves the parentheses of the first with-item.
7168
# It does so because it prefers splitting already parenthesized context managers, even if it leads to more parentheses
7269
# like in this case.
@@ -85,4 +82,8 @@
8582
with (aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c):
8683
pass
8784

88-
85+
with ( # outer comment
86+
CtxManager1(),
87+
CtxManager2(),
88+
):
89+
pass

crates/ruff_python_formatter/src/builders.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,6 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> {
219219
if let Some(last_end) = self.entries.position() {
220220
let magic_trailing_comma = has_magic_trailing_comma(
221221
TextRange::new(last_end, self.sequence_end),
222-
self.fmt.options(),
223222
self.fmt.context(),
224223
);
225224

crates/ruff_python_formatter/src/lib.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,14 +212,20 @@ if True:
212212
#[test]
213213
fn quick_test() {
214214
let source = r#"
215-
def main() -> None:
216-
if True:
217-
some_very_long_variable_name_abcdefghijk = Foo()
218-
some_very_long_variable_name_abcdefghijk = some_very_long_variable_name_abcdefghijk[
219-
some_very_long_variable_name_abcdefghijk.some_very_long_attribute_name
220-
== "This is a very long string abcdefghijk"
221-
]
215+
with (
216+
open(
217+
"/etc/hosts" # This is an incredibly long comment that has been replaced for sanitization
218+
)
219+
):
220+
pass
221+
222+
with open(
223+
"/etc/hosts" # This is an incredibly long comment that has been replaced for sanitization
224+
):
225+
pass
222226
227+
with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
228+
pass
223229
"#;
224230
let source_type = PySourceType::Python;
225231
let (tokens, comment_ranges) = tokens_and_ranges(source, source_type).unwrap();

crates/ruff_python_formatter/src/other/arguments.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,7 @@ fn is_arguments_huggable(arguments: &Arguments, context: &PyFormatContext) -> bo
205205

206206
// If the expression has a trailing comma, then we can't hug it.
207207
if options.magic_trailing_comma().is_respect()
208-
&& commas::has_magic_trailing_comma(
209-
TextRange::new(arg.end(), arguments.end()),
210-
options,
211-
context,
212-
)
208+
&& commas::has_magic_trailing_comma(TextRange::new(arg.end(), arguments.end()), context)
213209
{
214210
return false;
215211
}

crates/ruff_python_formatter/src/other/commas.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1+
use ruff_formatter::FormatContext;
12
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
23
use ruff_text_size::TextRange;
34

45
use crate::prelude::*;
5-
use crate::{MagicTrailingComma, PyFormatOptions};
6+
use crate::MagicTrailingComma;
67

78
/// Returns `true` if the range ends with a magic trailing comma (and the magic trailing comma
89
/// should be respected).
9-
pub(crate) fn has_magic_trailing_comma(
10-
range: TextRange,
11-
options: &PyFormatOptions,
12-
context: &PyFormatContext,
13-
) -> bool {
14-
match options.magic_trailing_comma() {
10+
pub(crate) fn has_magic_trailing_comma(range: TextRange, context: &PyFormatContext) -> bool {
11+
match context.options().magic_trailing_comma() {
1512
MagicTrailingComma::Respect => {
1613
let first_token = SimpleTokenizer::new(context.source(), range)
1714
.skip_trivia()

crates/ruff_python_formatter/src/other/with_item.rs

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ruff_formatter::write;
1+
use ruff_formatter::{write, FormatRuleWithOptions};
22
use ruff_python_ast::WithItem;
33

44
use crate::comments::SourceComment;
@@ -8,8 +8,57 @@ use crate::expression::parentheses::{
88
};
99
use crate::prelude::*;
1010

11+
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
12+
pub enum WithItemLayout {
13+
/// A with item that is the `with`s only context manager and its context expression is parenthesized.
14+
///
15+
/// ```python
16+
/// with (
17+
/// a + b
18+
/// ) as b:
19+
/// ...
20+
/// ```
21+
///
22+
/// This layout is used independent of the target version.
23+
SingleParenthesizedContextExpression,
24+
25+
/// This layout is used when the target python version doesn't support parenthesized context managers.
26+
Python38OrOlder,
27+
28+
/// A with item where the `with` formatting adds parentheses around all context managers if necessary.
29+
///
30+
/// ```python
31+
/// with (
32+
/// a,
33+
/// b,
34+
/// ): pass
35+
/// ```
36+
///
37+
/// This layout is generally used when the target version is Python 3.9 or newer but it is used
38+
/// for Python 3.8 if the with item has a leading or trailing comment.
39+
///
40+
/// ```python
41+
/// with (
42+
/// # leading
43+
/// a
44+
// ): ...
45+
/// ```
46+
#[default]
47+
ParenthesizedContextManagers,
48+
}
49+
1150
#[derive(Default)]
12-
pub struct FormatWithItem;
51+
pub struct FormatWithItem {
52+
layout: WithItemLayout,
53+
}
54+
55+
impl FormatRuleWithOptions<WithItem, PyFormatContext<'_>> for FormatWithItem {
56+
type Options = WithItemLayout;
57+
58+
fn with_options(self, options: Self::Options) -> Self {
59+
Self { layout: options }
60+
}
61+
}
1362

1463
impl FormatNodeRule<WithItem> for FormatWithItem {
1564
fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> {
@@ -28,40 +77,52 @@ impl FormatNodeRule<WithItem> for FormatWithItem {
2877
f.context().source(),
2978
);
3079

31-
// Remove the parentheses of the `with_items` if the with statement adds parentheses
32-
if f.context().node_level().is_parenthesized() {
33-
if is_parenthesized {
34-
// ...except if the with item is parenthesized, then use this with item as a preferred breaking point
35-
// or when it has comments, then parenthesize it to prevent comments from moving.
36-
maybe_parenthesize_expression(
37-
context_expr,
38-
item,
39-
Parenthesize::IfBreaksOrIfRequired,
40-
)
41-
.fmt(f)?;
42-
} else {
43-
context_expr
44-
.format()
45-
.with_options(Parentheses::Never)
80+
match self.layout {
81+
// Remove the parentheses of the `with_items` if the with statement adds parentheses
82+
WithItemLayout::ParenthesizedContextManagers => {
83+
if is_parenthesized {
84+
// ...except if the with item is parenthesized, then use this with item as a preferred breaking point
85+
// or when it has comments, then parenthesize it to prevent comments from moving.
86+
maybe_parenthesize_expression(
87+
context_expr,
88+
item,
89+
Parenthesize::IfBreaksOrIfRequired,
90+
)
4691
.fmt(f)?;
92+
} else {
93+
context_expr
94+
.format()
95+
.with_options(Parentheses::Never)
96+
.fmt(f)?;
97+
}
98+
}
99+
100+
WithItemLayout::SingleParenthesizedContextExpression => {
101+
write!(
102+
f,
103+
[maybe_parenthesize_expression(
104+
context_expr,
105+
item,
106+
Parenthesize::IfBreaks
107+
)]
108+
)?;
109+
}
110+
111+
WithItemLayout::Python38OrOlder => {
112+
let parenthesize = if is_parenthesized {
113+
Parenthesize::IfBreaks
114+
} else {
115+
Parenthesize::IfRequired
116+
};
117+
write!(
118+
f,
119+
[maybe_parenthesize_expression(
120+
context_expr,
121+
item,
122+
parenthesize
123+
)]
124+
)?;
47125
}
48-
} else {
49-
// Prefer keeping parentheses for already parenthesized expressions over
50-
// parenthesizing other nodes.
51-
let parenthesize = if is_parenthesized {
52-
Parenthesize::IfBreaks
53-
} else {
54-
Parenthesize::IfRequired
55-
};
56-
57-
write!(
58-
f,
59-
[maybe_parenthesize_expression(
60-
context_expr,
61-
item,
62-
parenthesize
63-
)]
64-
)?;
65126
}
66127

67128
if let Some(optional_vars) = optional_vars {

0 commit comments

Comments
 (0)