Skip to content

Commit 3a2c3a7

Browse files
konstinMichaReiser
andauthored
Format empty lines in stub files like black's preview style (#7206)
## Summary Fix all but one empty line differences with the black preview style in typeshed. The remaining differences are breaking with type comments and trailing commas in function definitions. I compared the empty line differences with the preview mode of black since stable has some oddities that would have been hard to replicate (psf/black#3861). Additionally, it assumes the style proposed in psf/black#3862. An edge case that also surfaced with typeshed are newline before trailing module comments. **main** | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76083 | 1789 | 1632 | | django | 0.99966 | 2760 | 58 | | transformers | 0.99930 | 2587 | 447 | | twine | 1.00000 | 33 | 0 | | **typeshed** | 0.99978 | 3496 | **2173** | | warehouse | 0.99825 | 648 | 22 | | zulip | 0.99950 | 1437 | 27 | **PR** | project | similarity index | total files | changed files | |--------------|------------------:|------------------:|------------------:| | cpython | 0.76083 | 1789 | 1632 | | django | 0.99966 | 2760 | 58 | | transformers | 0.99930 | 2587 | 447 | | twine | 1.00000 | 33 | 0 | | **typeshed** | 0.99983 | 3496 | **18** | | warehouse | 0.99825 | 648 | 22 | | zulip | 0.99950 | 1437 | 27 | Closes #6723 ## Test Plan The main driver was the typeshed diff. I added new test cases for all kinds of possible empty line combinations in stub files, test cases for newlines before trailing module comments. --------- Co-authored-by: Micha Reiser <[email protected]>
1 parent 7440e54 commit 3a2c3a7

18 files changed

+857
-76
lines changed

.editorconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ indent_style = space
1010
insert_final_newline = true
1111
indent_size = 2
1212

13-
[*.{rs,py}]
13+
[*.{rs,py,pyi}]
1414
indent_size = 4
1515

1616
[*.snap]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
2+
3+
# Shared types throughout the stub
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__all__ = ["X", "XK", "Xatom", "Xcursorfont", "Xutil", "display", "error", "rdb"]
2+
# Shared types throughout the stub
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class SupportsAnext:
2+
def __anext__(self): ...
3+
4+
# Comparison protocols
5+
6+
class SupportsDunderLT:
7+
def __init__(self): ...
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Tests specifically for https://github.com/psf/black/issues/3861"""
2+
3+
import sys
4+
5+
6+
class OuterClassOrOtherSuite:
7+
class Nested11:
8+
class Nested12:
9+
assignment = 1
10+
def function_definition(self): ...
11+
12+
def f1(self) -> str: ...
13+
14+
class Nested21:
15+
class Nested22:
16+
def function_definition(self): ...
17+
assignment = 1
18+
19+
def f2(self) -> str: ...
20+
21+
if sys.version_info > (3, 7):
22+
if sys.platform == "win32":
23+
assignment = 1
24+
def function_definition(self): ...
25+
26+
def f1(self) -> str: ...
27+
if sys.platform != "win32":
28+
def function_definition(self): ...
29+
assignment = 1
30+
31+
def f2(self) -> str: ...
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Tests for empty line rules in stub files, mostly inspired by typeshed.
2+
The rules are a list of nested exceptions. See also
3+
https://github.com/psf/black/blob/c160e4b7ce30c661ac4f2dfa5038becf1b8c5c33/src/black/lines.py#L576-L744
4+
"""
5+
6+
import sys
7+
from typing import Self, TypeAlias, final
8+
9+
if sys.version_info >= (3, 8):
10+
class InnerClass1: ...
11+
12+
class InnerClass2:
13+
def a(self): ...
14+
15+
class InnerClass3:
16+
def a(self): ...
17+
18+
class InnerClass4: ...
19+
details: int
20+
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
21+
details: int
22+
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
23+
@final
24+
class DecoratorInsteadOfEmptyLine: ...
25+
26+
def open(device: str) -> None: ...
27+
28+
# oss_mixer_device return type
29+
def openmixer(device: str = ...) -> None: ...
30+
def open2(device: str) -> None: ...
31+
# oss_mixer_device2 return type
32+
def openmixer2(device: str = ...) -> None: ...
33+
34+
else:
35+
class Slice1: ...
36+
_Slice1: TypeAlias = Slice1
37+
38+
class Slice2: ...
39+
_Slice2: TypeAlias = Slice2
40+
41+
class NoEmptyLinesBetweenFunctions:
42+
def multi_line_but_only_ellipsis(
43+
self,
44+
mandatory_release: float | None,
45+
) -> None: ...
46+
def only_ellipsis1(self) -> float: ...
47+
def only_ellipsis2(self) -> float | None: ...
48+
def has_impl1(self):
49+
print(self)
50+
return 1
51+
52+
def has_impl2(self):
53+
print(self)
54+
return 2
55+
56+
def no_impl4(self): ...
57+
58+
class NoEmptyLinesBetweenField:
59+
field1: int
60+
field2: (
61+
# type
62+
int
63+
)
64+
field3 = 3
65+
field4 = (
66+
1,
67+
2,
68+
)
69+
field5 = 5
70+
71+
class FieldAndFunctionsWithOptionalEmptyLines:
72+
details1: int
73+
def f1(self, hresult: int, text: str | None, detail: int) -> None: ...
74+
details2: int
75+
def f2(self, hresult: int, text: str | None, detail: int) -> None: ...
76+
details3: int
77+
78+
class NewlinesBetweenStubInnerClasses:
79+
def f1(self): ...
80+
81+
class InnerClass1: ...
82+
class InnerClass2: ...
83+
84+
def f2(self): ...
85+
86+
class InnerClass3: ...
87+
class InnerClass4: ...
88+
field = 1
89+
90+
class InnerClass3: ...
91+
class InnerClass4: ...
92+
93+
def f3(self): ...
94+
@final
95+
class DecoratorInsteadOfEmptyLine: ...
96+
97+
@final
98+
class DecoratorStillEmptyLine: ...
99+
100+
class NewlinesBetweenInnerClasses:
101+
class InnerClass1: ...
102+
103+
class InnerClass2:
104+
def a(self): ...
105+
106+
class InnerClass3:
107+
def a(self): ...
108+
109+
class InnerClass4: ...
110+
111+
class InnerClass5:
112+
def a(self): ...
113+
field1 = 1
114+
115+
class InnerClass6:
116+
def a(self): ...
117+
118+
def f1(self): ...
119+
120+
class InnerClass7:
121+
def a(self): ...
122+
print("hi")
123+
124+
class InnerClass8:
125+
def a(self): ...
126+
127+
class ComplexStatements:
128+
# didn't match the name in the C implementation,
129+
# meaning it is only *safe* to pass it as a keyword argument on 3.12+
130+
if sys.version_info >= (3, 12):
131+
@classmethod
132+
def fromtimestamp(cls, timestamp: float, tz: float | None = ...) -> Self: ...
133+
else:
134+
@classmethod
135+
def fromtimestamp(cls, __timestamp: float, tz: float | None = ...) -> Self: ...
136+
137+
@classmethod
138+
def utcfromtimestamp(cls, __t: float) -> Self: ...
139+
if sys.version_info >= (3, 8):
140+
@classmethod
141+
def now(cls, tz: float | None = None) -> Self: ...
142+
else:
143+
@classmethod
144+
def now(cls, tz: None = None) -> Self: ...
145+
@classmethod
146+
def now2(cls, tz: float) -> Self: ...
147+
148+
@classmethod
149+
def utcnow(cls) -> Self: ...
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from typing import final
2+
3+
4+
def count1(): ...
5+
def count2(): ...
6+
@final
7+
def count3(): ...
8+
@final
9+
class LockType1: ...
10+
11+
def count4(): ...
12+
13+
class LockType2: ...
14+
class LockType3: ...
15+
16+
@final
17+
class LockType4: ...

crates/ruff_python_formatter/src/comments/format.rs

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use std::borrow::Cow;
22

33
use ruff_formatter::{format_args, write, FormatError, FormatOptions, SourceCode};
44
use ruff_python_ast::node::{AnyNodeRef, AstNode};
5+
use ruff_python_ast::PySourceType;
56
use ruff_python_trivia::{lines_after, lines_after_ignoring_trivia, lines_before};
67
use ruff_text_size::{Ranged, TextLen, TextRange};
78

@@ -485,19 +486,30 @@ fn strip_comment_prefix(comment_text: &str) -> FormatResult<&str> {
485486
///
486487
/// This builder will insert two empty lines before the comment.
487488
/// ```
488-
pub(crate) const fn empty_lines_before_trailing_comments(
489-
comments: &[SourceComment],
490-
expected: u32,
491-
) -> FormatEmptyLinesBeforeTrailingComments {
492-
FormatEmptyLinesBeforeTrailingComments { comments, expected }
489+
pub(crate) fn empty_lines_before_trailing_comments<'a>(
490+
f: &PyFormatter,
491+
comments: &'a [SourceComment],
492+
) -> FormatEmptyLinesBeforeTrailingComments<'a> {
493+
// Black has different rules for stub vs. non-stub and top level vs. indented
494+
let empty_lines = match (f.options().source_type(), f.context().node_level()) {
495+
(PySourceType::Stub, NodeLevel::TopLevel) => 1,
496+
(PySourceType::Stub, _) => 0,
497+
(_, NodeLevel::TopLevel) => 2,
498+
(_, _) => 1,
499+
};
500+
501+
FormatEmptyLinesBeforeTrailingComments {
502+
comments,
503+
empty_lines,
504+
}
493505
}
494506

495507
#[derive(Copy, Clone, Debug)]
496508
pub(crate) struct FormatEmptyLinesBeforeTrailingComments<'a> {
497509
/// The trailing comments of the node.
498510
comments: &'a [SourceComment],
499511
/// The expected number of empty lines before the trailing comments.
500-
expected: u32,
512+
empty_lines: u32,
501513
}
502514

503515
impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_> {
@@ -508,7 +520,7 @@ impl Format<PyFormatContext<'_>> for FormatEmptyLinesBeforeTrailingComments<'_>
508520
.find(|comment| comment.line_position().is_own_line())
509521
{
510522
let actual = lines_before(comment.start(), f.context().source()).saturating_sub(1);
511-
for _ in actual..self.expected {
523+
for _ in actual..self.empty_lines {
512524
write!(f, [empty_line()])?;
513525
}
514526
}
Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
11
use ruff_formatter::prelude::hard_line_break;
2-
use ruff_formatter::write;
2+
use ruff_formatter::{Buffer, FormatResult};
33
use ruff_python_ast::ModModule;
44

5-
use crate::prelude::*;
5+
use crate::comments::{trailing_comments, SourceComment};
66
use crate::statement::suite::SuiteKind;
7+
use crate::{write, AsFormat, FormatNodeRule, PyFormatter};
78

89
#[derive(Default)]
910
pub struct FormatModModule;
1011

1112
impl FormatNodeRule<ModModule> for FormatModModule {
1213
fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> {
1314
let ModModule { range: _, body } = item;
15+
let comments = f.context().comments().clone();
16+
1417
write!(
1518
f,
1619
[
1720
body.format().with_options(SuiteKind::TopLevel),
21+
trailing_comments(comments.dangling(item)),
1822
// Trailing newline at the end of the file
1923
hard_line_break()
2024
]
2125
)
2226
}
27+
28+
fn fmt_dangling_comments(
29+
&self,
30+
_dangling_comments: &[SourceComment],
31+
_f: &mut PyFormatter,
32+
) -> FormatResult<()> {
33+
// Handled as part of `fmt_fields`
34+
Ok(())
35+
}
2336
}

crates/ruff_python_formatter/src/statement/stmt_class_def.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use ruff_text_size::Ranged;
55

66
use crate::comments::format::empty_lines_before_trailing_comments;
77
use crate::comments::{leading_comments, trailing_comments, SourceComment};
8-
use crate::context::NodeLevel;
98
use crate::prelude::*;
109
use crate::statement::clause::{clause_body, clause_header, ClauseHeader};
1110
use crate::statement::suite::SuiteKind;
@@ -120,23 +119,15 @@ impl FormatNodeRule<StmtClassDef> for FormatStmtClassDef {
120119
// # comment
121120
// ```
122121
//
123-
// At the top-level, reformat as:
122+
// At the top-level in a non-stub file, reformat as:
124123
// ```python
125124
// class Class:
126125
// ...
127126
//
128127
//
129128
// # comment
130129
// ```
131-
empty_lines_before_trailing_comments(
132-
comments.trailing(item),
133-
if f.context().node_level() == NodeLevel::TopLevel {
134-
2
135-
} else {
136-
1
137-
},
138-
)
139-
.fmt(f)
130+
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
140131
}
141132

142133
fn fmt_dangling_comments(

crates/ruff_python_formatter/src/statement/stmt_function_def.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
55
use ruff_text_size::Ranged;
66

77
use crate::comments::SourceComment;
8-
use crate::context::NodeLevel;
98
use crate::expression::maybe_parenthesize_expression;
109
use crate::expression::parentheses::{Parentheses, Parenthesize};
1110
use crate::prelude::*;
@@ -156,23 +155,15 @@ impl FormatNodeRule<StmtFunctionDef> for FormatStmtFunctionDef {
156155
// # comment
157156
// ```
158157
//
159-
// At the top-level, reformat as:
158+
// At the top-level in a non-stub file, reformat as:
160159
// ```python
161160
// def func():
162161
// ...
163162
//
164163
//
165164
// # comment
166165
// ```
167-
empty_lines_before_trailing_comments(
168-
comments.trailing(item),
169-
if f.context().node_level() == NodeLevel::TopLevel {
170-
2
171-
} else {
172-
1
173-
},
174-
)
175-
.fmt(f)
166+
empty_lines_before_trailing_comments(f, comments.trailing(item)).fmt(f)
176167
}
177168

178169
fn fmt_dangling_comments(

0 commit comments

Comments
 (0)