Skip to content

Commit 541aef4

Browse files
authored
Implement blank_line_after_nested_stub_class preview style (#9155)
## Summary This PR implements the `blank_line_after_nested_stub_class` preview style in the formatter. The logic is divided into 3 parts: 1. In between preceding and following nodes at top level and nested suite 2. When there's a trailing comment after the class 3. When there is no following node from (1) which is the case when it's the last or the only node in a suite We handle (3) with `FormatLeadingAlternateBranchComments`. ## Test Plan - Add new test cases and update existing snapshots - Checked the `typeshed` diff fixes: #8891
1 parent 79f0522 commit 541aef4

13 files changed

Lines changed: 891 additions & 29 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"source_type": "Stub",
4+
"preview": "enabled"
5+
}
6+
]
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
class Top1:
2+
pass
3+
class Top2:
4+
pass
5+
6+
class Top:
7+
class Ellipsis: ...
8+
class Ellipsis: ...
9+
10+
class Top:
11+
class Ellipsis: ...
12+
class Pass:
13+
pass
14+
15+
class Top:
16+
class Ellipsis: ...
17+
class_variable = 1
18+
19+
class Top:
20+
class TrailingComment:
21+
pass
22+
# comment
23+
class Other:
24+
pass
25+
26+
class Top:
27+
class CommentWithEllipsis: ...
28+
# comment
29+
class Other: ...
30+
31+
class Top:
32+
class TrailingCommentWithMultipleBlankLines:
33+
pass
34+
35+
36+
# comment
37+
class Other:
38+
pass
39+
40+
class Top:
41+
class Nested:
42+
pass
43+
44+
# comment
45+
class LeadingComment:
46+
pass
47+
48+
class Top:
49+
@decorator
50+
class Ellipsis: ...
51+
class Ellipsis: ...
52+
53+
class Top:
54+
@decorator
55+
class Ellipsis: ...
56+
@decorator
57+
class Ellipsis: ...
58+
59+
class Top:
60+
@decorator
61+
class Ellipsis: ...
62+
@decorator
63+
class Pass:
64+
pass
65+
66+
class Top:
67+
class Foo:
68+
pass
69+
70+
71+
72+
73+
class AfterMultipleEmptyLines:
74+
pass
75+
76+
class Top:
77+
class Nested11:
78+
class Nested12:
79+
pass
80+
class Nested21:
81+
pass
82+
83+
class Top:
84+
class Nested11:
85+
class Nested12:
86+
pass
87+
# comment
88+
class Nested21:
89+
pass
90+
91+
class Top:
92+
class Nested11:
93+
class Nested12:
94+
pass
95+
# comment
96+
class Nested21:
97+
pass
98+
# comment
99+
100+
class Top1:
101+
class Nested:
102+
pass
103+
class Top2:
104+
pass
105+
106+
class Top1:
107+
class Nested:
108+
pass
109+
# comment
110+
class Top2:
111+
pass
112+
113+
class Top1:
114+
class Nested:
115+
pass
116+
# comment
117+
class Top2:
118+
pass
119+
120+
if foo:
121+
class Nested1:
122+
pass
123+
class Nested2:
124+
pass
125+
else:
126+
pass
127+
128+
if foo:
129+
class Nested1:
130+
pass
131+
class Nested2:
132+
pass
133+
# comment
134+
elif bar:
135+
class Nested1:
136+
pass
137+
# comment
138+
else:
139+
pass
140+
141+
if top1:
142+
class Nested:
143+
pass
144+
if top2:
145+
pass
146+
147+
if top1:
148+
class Nested:
149+
pass
150+
# comment
151+
if top2:
152+
pass
153+
154+
if top1:
155+
class Nested:
156+
pass
157+
# comment
158+
if top2:
159+
pass
160+
161+
try:
162+
class Try:
163+
pass
164+
except:
165+
class Except:
166+
pass
167+
foo = 1
168+
169+
match foo:
170+
case 1:
171+
class Nested:
172+
pass
173+
case 2:
174+
class Nested:
175+
pass
176+
case _:
177+
class Nested:
178+
pass
179+
foo = 1
180+
181+
class Eof:
182+
class Nested:
183+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"source_type": "Stub",
4+
"preview": "enabled"
5+
}
6+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# A separate file to test out the behavior when there are a mix of blank lines
2+
# and comments at EOF just after a nested stub class.
3+
4+
class Top:
5+
class Nested1:
6+
class Nested12:
7+
pass
8+
# comment
9+
class Nested2:
10+
pass
11+
12+
13+
14+
# comment
15+
16+
17+

crates/ruff_python_formatter/src/comments/format.rs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use std::borrow::Cow;
22

33
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
4-
use ruff_python_ast::PySourceType;
5-
use ruff_python_ast::{AnyNodeRef, AstNode};
4+
use ruff_python_ast::{AnyNodeRef, AstNode, NodeKind, PySourceType};
65
use ruff_python_trivia::{
76
is_pragma_comment, lines_after, lines_after_ignoring_trivia, lines_before,
87
};
@@ -11,6 +10,8 @@ use ruff_text_size::{Ranged, TextLen, TextRange};
1110
use crate::comments::{CommentLinePosition, SourceComment};
1211
use crate::context::NodeLevel;
1312
use crate::prelude::*;
13+
use crate::preview::is_blank_line_after_nested_stub_class_enabled;
14+
use crate::statement::suite::should_insert_blank_line_after_class_in_stub_file;
1415

1516
/// Formats the leading comments of a node.
1617
pub(crate) fn leading_node_comments<T>(node: &T) -> FormatLeadingComments
@@ -85,7 +86,11 @@ pub(crate) struct FormatLeadingAlternateBranchComments<'a> {
8586

8687
impl Format<PyFormatContext<'_>> for FormatLeadingAlternateBranchComments<'_> {
8788
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
88-
if let Some(first_leading) = self.comments.first() {
89+
if self.last_node.map_or(false, |preceding| {
90+
should_insert_blank_line_after_class_in_stub_file(preceding, None, f.context())
91+
}) {
92+
write!(f, [empty_line(), leading_comments(self.comments)])?;
93+
} else if let Some(first_leading) = self.comments.first() {
8994
// Leading comments only preserves the lines after the comment but not before.
9095
// Insert the necessary lines.
9196
write!(
@@ -513,14 +518,32 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
513518
/// ```
514519
///
515520
/// This builder will insert two empty lines before the comment.
521+
///
522+
/// # Preview
523+
///
524+
/// For preview style, this builder will insert a single empty line after a
525+
/// class definition in a stub file.
526+
///
527+
/// For example, given:
528+
/// ```python
529+
/// class Foo:
530+
/// pass
531+
/// # comment
532+
/// ```
533+
///
534+
/// This builder will insert a single empty line before the comment.
516535
pub(crate) fn empty_lines_before_trailing_comments<'a>(
517536
f: &PyFormatter,
518537
comments: &'a [SourceComment],
538+
node_kind: NodeKind,
519539
) -> FormatEmptyLinesBeforeTrailingComments<'a> {
520540
// Black has different rules for stub vs. non-stub and top level vs. indented
521541
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
522542
(PySourceType::Stub, NodeLevel::TopLevel(_)) => 1,
523-
(PySourceType::Stub, _) => 0,
543+
(PySourceType::Stub, _) => u32::from(
544+
is_blank_line_after_nested_stub_class_enabled(f.context())
545+
&& node_kind == NodeKind::StmtClassDef,
546+
),
524547
(_, NodeLevel::TopLevel(_)) => 2,
525548
(_, _) => 1,
526549
};

crates/ruff_python_formatter/src/preview.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ pub(crate) const fn is_wrap_multiple_context_managers_in_parens_enabled(
4848
context.is_preview()
4949
}
5050

51+
/// Returns `true` if the [`blank_line_after_nested_stub_class`](https://github.com/astral-sh/ruff/issues/8891) preview style is enabled.
52+
pub(crate) const fn is_blank_line_after_nested_stub_class_enabled(
53+
context: &PyFormatContext,
54+
) -> bool {
55+
context.is_preview()
56+
}
57+
5158
/// Returns `true` if the [`module_docstring_newlines`](https://github.com/astral-sh/ruff/issues/7995) preview style is enabled.
5259
pub(crate) const fn is_module_docstring_newlines_enabled(context: &PyFormatContext) -> bool {
5360
context.is_preview()

crates/ruff_python_formatter/src/statement/stmt_class_def.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use ruff_formatter::write;
2-
use ruff_python_ast::{Decorator, StmtClassDef};
2+
use ruff_python_ast::{Decorator, NodeKind, StmtClassDef};
33
use ruff_python_trivia::lines_after_ignoring_end_of_line_trivia;
44
use ruff_text_size::Ranged;
55

@@ -152,7 +152,10 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
152152
//
153153
// # comment
154154
// ```
155-
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
155+
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtClassDef)
156+
.fmt(f)?;
157+
158+
Ok(())
156159
}
157160

158161
fn fmt_dangling_comments(

crates/ruff_python_formatter/src/statement/stmt_function_def.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use ruff_formatter::write;
2-
use ruff_python_ast::StmtFunctionDef;
2+
use ruff_python_ast::{NodeKind, StmtFunctionDef};
33

44
use crate::comments::format::{
55
empty_lines_after_leading_comments, empty_lines_before_trailing_comments,
@@ -87,7 +87,8 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
8787
//
8888
// # comment
8989
// ```
90-
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
90+
empty_lines_before_trailing_comments(f, comments.trailing(item), NodeKind::StmtFunctionDef)
91+
.fmt(f)
9192
}
9293

9394
fn fmt_dangling_comments(

0 commit comments

Comments
 (0)