Skip to content

Commit a56d42f

Browse files
authored
Refactor with statement formatting to have explicit layouts (#10296)
## Summary This PR refactors the with item formatting to use more explicit layouts to make it easier to understand the different formatting cases. The benefit of the explicit layout is that it makes it easier to reasons about layout transition between format runs. For example, today it's possible that `SingleWithTarget` or `ParenthesizeIfExpands` add parentheses around the with items for `with aaaaaaaaaa + bbbbbbbbbbbb: pass`, resulting in `with (aaaaaaaaaa + bbbbbbbbbbbb): pass`. The problem with this is that the next formatting pass uses the `SingleParenthesizedContextExpression` layout that uses `maybe_parenthesize_expression` which is different from `parenthesize_if_expands(&expr)` or `optional_parentheses(&expr)`. ## Test Plan `cargo test` I ran the ecosystem checks locally and there are no changes.
1 parent 1d97f27 commit a56d42f

File tree

5 files changed

+303
-164
lines changed

5 files changed

+303
-164
lines changed

crates/ruff_python_formatter/src/builders.rs

-1
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/other/arguments.rs

+1-5
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

+4-7
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

+104-34
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,66 @@ 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+
SingleParenthesizedContextManager,
24+
25+
/// This layout is used when the target python version doesn't support parenthesized context managers and
26+
/// it's either a single, unparenthesized with item or multiple items.
27+
///
28+
/// ```python
29+
/// with a + b:
30+
/// ...
31+
///
32+
/// with a, b:
33+
/// ...
34+
/// ```
35+
Python38OrOlder,
36+
37+
/// A with item where the `with` formatting adds parentheses around all context managers if necessary.
38+
///
39+
/// ```python
40+
/// with (
41+
/// a,
42+
/// b,
43+
/// ): pass
44+
/// ```
45+
///
46+
/// This layout is generally used when the target version is Python 3.9 or newer, but it is used
47+
/// for Python 3.8 if the with item has a leading or trailing comment.
48+
///
49+
/// ```python
50+
/// with (
51+
/// # leading
52+
/// a
53+
// ): ...
54+
/// ```
55+
#[default]
56+
ParenthesizedContextManagers,
57+
}
58+
1159
#[derive(Default)]
12-
pub struct FormatWithItem;
60+
pub struct FormatWithItem {
61+
layout: WithItemLayout,
62+
}
63+
64+
impl FormatRuleWithOptions<WithItem, PyFormatContext<'_>> for FormatWithItem {
65+
type Options = WithItemLayout;
66+
67+
fn with_options(self, options: Self::Options) -> Self {
68+
Self { layout: options }
69+
}
70+
}
1371

1472
impl FormatNodeRule<WithItem> for FormatWithItem {
1573
fn fmt_fields(&self, item: &WithItem, f: &mut PyFormatter) -> FormatResult<()> {
@@ -28,40 +86,52 @@ impl FormatNodeRule<WithItem> for FormatWithItem {
2886
f.context().source(),
2987
);
3088

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)
89+
match self.layout {
90+
// Remove the parentheses of the `with_items` if the with statement adds parentheses
91+
WithItemLayout::ParenthesizedContextManagers => {
92+
if is_parenthesized {
93+
// ...except if the with item is parenthesized, then use this with item as a preferred breaking point
94+
// or when it has comments, then parenthesize it to prevent comments from moving.
95+
maybe_parenthesize_expression(
96+
context_expr,
97+
item,
98+
Parenthesize::IfBreaksOrIfRequired,
99+
)
46100
.fmt(f)?;
101+
} else {
102+
context_expr
103+
.format()
104+
.with_options(Parentheses::Never)
105+
.fmt(f)?;
106+
}
107+
}
108+
109+
WithItemLayout::SingleParenthesizedContextManager => {
110+
write!(
111+
f,
112+
[maybe_parenthesize_expression(
113+
context_expr,
114+
item,
115+
Parenthesize::IfBreaks
116+
)]
117+
)?;
118+
}
119+
120+
WithItemLayout::Python38OrOlder => {
121+
let parenthesize = if is_parenthesized {
122+
Parenthesize::IfBreaks
123+
} else {
124+
Parenthesize::IfRequired
125+
};
126+
write!(
127+
f,
128+
[maybe_parenthesize_expression(
129+
context_expr,
130+
item,
131+
parenthesize
132+
)]
133+
)?;
47134
}
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-
)?;
65135
}
66136

67137
if let Some(optional_vars) = optional_vars {

0 commit comments

Comments
 (0)