Skip to content

Commit 317d3dd

Browse files
authored
Add test and basic implementation for formatter preview mode (#8044)
**Summary** Prepare for the black preview style becoming the black stable style at the end of the year. This adds a new test file to compare stable and preview on some relevant preview options in black, and makes `format_dev` understand the black preview flag. I've added poetry as a project that uses preview. I've implemented one specific deviation (collapsing of stub implementation in non-stub files) which showed up in poetry for testing. This also improves poetry compatibility from 0.99891 to 0.99919. Fixes #7440 New compatibility stats: | project | similarity index | total files | changed files | |----------------|------------------:|------------------:|------------------:| | cpython | 0.75803 | 1799 | 1647 | | django | 0.99983 | 2772 | 35 | | home-assistant | 0.99953 | 10596 | 189 | | poetry | 0.99919 | 317 | 12 | | transformers | 0.99963 | 2657 | 332 | | twine | 1.00000 | 33 | 0 | | typeshed | 0.99978 | 3669 | 20 | | warehouse | 0.99969 | 654 | 15 | | zulip | 0.99970 | 1459 | 22 |
1 parent f5e8507 commit 317d3dd

16 files changed

+790
-32
lines changed

crates/ruff_dev/src/format_dev.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use ruff_formatter::{FormatError, LineWidth, PrintError};
3333
use ruff_linter::logging::LogLevel;
3434
use ruff_linter::settings::types::{FilePattern, FilePatternSet};
3535
use ruff_python_formatter::{
36-
format_module_source, FormatModuleError, MagicTrailingComma, PyFormatOptions,
36+
format_module_source, FormatModuleError, MagicTrailingComma, PreviewMode, PyFormatOptions,
3737
};
3838
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver};
3939

@@ -871,17 +871,15 @@ struct BlackOptions {
871871
line_length: NonZeroU16,
872872
#[serde(alias = "skip-magic-trailing-comma")]
873873
skip_magic_trailing_comma: bool,
874-
#[allow(unused)]
875-
#[serde(alias = "force-exclude")]
876-
force_exclude: Option<String>,
874+
preview: bool,
877875
}
878876

879877
impl Default for BlackOptions {
880878
fn default() -> Self {
881879
Self {
882880
line_length: NonZeroU16::new(88).unwrap(),
883881
skip_magic_trailing_comma: false,
884-
force_exclude: None,
882+
preview: false,
885883
}
886884
}
887885
}
@@ -929,6 +927,11 @@ impl BlackOptions {
929927
} else {
930928
MagicTrailingComma::Respect
931929
})
930+
.with_preview(if self.preview {
931+
PreviewMode::Enabled
932+
} else {
933+
PreviewMode::Disabled
934+
})
932935
}
933936
}
934937

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,39 @@
1-
# Below is black stable style
2-
# In preview style, black always breaks the right side first
1+
"""
2+
Black's `Preview.module_docstring_newlines`
3+
"""
4+
first_stmt_after_module_level_docstring = 1
35

4-
if True:
6+
7+
class CachedRepository:
8+
# Black's `Preview.dummy_implementations`
9+
def get_release_info(self): ...
10+
11+
12+
def raw_docstring():
13+
14+
r"""Black's `Preview.accept_raw_docstrings`
15+
a
16+
b
17+
"""
18+
pass
19+
20+
21+
def reference_docstring_newlines():
22+
23+
"""A regular docstring for comparison
24+
a
25+
b
26+
"""
27+
pass
28+
29+
30+
class RemoveNewlineBeforeClassDocstring:
31+
32+
"""Black's `Preview.no_blank_line_before_class_docstring`"""
33+
34+
35+
def f():
36+
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
537
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
638
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
739
] = cccccccc.ccccccccccccc.cccccccc

crates/ruff_python_formatter/src/cli.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use ruff_python_parser::{parse_ok_tokens, AsMode};
1212
use ruff_text_size::Ranged;
1313

1414
use crate::comments::collect_comments;
15-
use crate::{format_module_ast, PyFormatOptions};
15+
use crate::{format_module_ast, PreviewMode, PyFormatOptions};
1616

1717
#[derive(ValueEnum, Clone, Debug)]
1818
pub enum Emit {
@@ -24,6 +24,7 @@ pub enum Emit {
2424

2525
#[derive(Parser)]
2626
#[command(author, version, about, long_about = None)]
27+
#[allow(clippy::struct_excessive_bools)] // It's only the dev cli anyways
2728
pub struct Cli {
2829
/// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported
2930
pub files: Vec<PathBuf>,
@@ -34,6 +35,8 @@ pub struct Cli {
3435
#[clap(long)]
3536
pub check: bool,
3637
#[clap(long)]
38+
pub preview: bool,
39+
#[clap(long)]
3740
pub print_ir: bool,
3841
#[clap(long)]
3942
pub print_comments: bool,
@@ -48,7 +51,11 @@ pub fn format_and_debug_print(source: &str, cli: &Cli, source_path: &Path) -> Re
4851
let module = parse_ok_tokens(tokens, source, source_type.as_mode(), "<filename>")
4952
.context("Syntax error in input")?;
5053

51-
let options = PyFormatOptions::from_extension(source_path);
54+
let options = PyFormatOptions::from_extension(source_path).with_preview(if cli.preview {
55+
PreviewMode::Enabled
56+
} else {
57+
PreviewMode::Disabled
58+
});
5259

5360
let source_code = SourceCode::new(source);
5461
let formatted = format_module_ast(&module, &comment_ranges, source, options)

crates/ruff_python_formatter/src/statement/clause.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,9 @@ pub(crate) fn clause_body<'a>(
391391

392392
impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
393393
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
394-
if f.options().source_type().is_stub()
394+
// In stable, stubs are only collapsed in stub files, in preview this is consistently
395+
// applied everywhere
396+
if (f.options().source_type().is_stub() || f.options().preview().is_enabled())
395397
&& contains_only_an_ellipsis(self.body, f.context().comments())
396398
&& self.trailing_comments.is_empty()
397399
{

crates/ruff_python_formatter/tests/fixtures.rs

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use ruff_formatter::FormatOptions;
2-
use ruff_python_formatter::{format_module_source, PyFormatOptions};
2+
use ruff_python_formatter::{format_module_source, PreviewMode, PyFormatOptions};
33
use similar::TextDiff;
44
use std::fmt::{Formatter, Write};
55
use std::io::BufReader;
@@ -142,16 +142,40 @@ fn format() {
142142
} else {
143143
let printed =
144144
format_module_source(&content, options.clone()).expect("Formatting to succeed");
145-
let formatted_code = printed.as_code();
145+
let formatted = printed.as_code();
146146

147-
ensure_stability_when_formatting_twice(formatted_code, options, input_path);
147+
ensure_stability_when_formatting_twice(formatted, options.clone(), input_path);
148148

149-
writeln!(
150-
snapshot,
151-
"## Output\n{}",
152-
CodeFrame::new("py", &formatted_code)
153-
)
154-
.unwrap();
149+
// We want to capture the differences in the preview style in our fixtures
150+
let options_preview = options.with_preview(PreviewMode::Enabled);
151+
let printed_preview = format_module_source(&content, options_preview.clone())
152+
.expect("Formatting to succeed");
153+
let formatted_preview = printed_preview.as_code();
154+
155+
ensure_stability_when_formatting_twice(
156+
formatted_preview,
157+
options_preview.clone(),
158+
input_path,
159+
);
160+
161+
if formatted == formatted_preview {
162+
writeln!(snapshot, "## Output\n{}", CodeFrame::new("py", &formatted)).unwrap();
163+
} else {
164+
// Having both snapshots makes it hard to see the difference, so we're keeping only
165+
// diff.
166+
writeln!(
167+
snapshot,
168+
"## Output\n{}\n## Preview changes\n{}",
169+
CodeFrame::new("py", &formatted),
170+
CodeFrame::new(
171+
"diff",
172+
TextDiff::from_lines(formatted, formatted_preview)
173+
.unified_diff()
174+
.header("Stable", "Preview")
175+
)
176+
)
177+
.unwrap();
178+
}
155179
}
156180

157181
insta::with_settings!({

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,21 @@ def test3 ():
9393
```
9494

9595

96+
## Preview changes
97+
```diff
98+
--- Stable
99+
+++ Preview
100+
@@ -21,8 +21,7 @@
101+
102+
103+
# formatted
104+
-def test2():
105+
- ...
106+
+def test2(): ...
107+
108+
109+
a = 10
110+
```
111+
112+
96113

crates/ruff_python_formatter/tests/snapshots/[email protected]

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,4 +549,27 @@ if True:
549549
```
550550

551551

552+
## Preview changes
553+
```diff
554+
--- Stable
555+
+++ Preview
556+
@@ -245,13 +245,11 @@
557+
class Path:
558+
if sys.version_info >= (3, 11):
559+
560+
- def joinpath(self):
561+
- ...
562+
+ def joinpath(self): ...
563+
564+
# The .open method comes from pathlib.pyi and should be kept in sync.
565+
@overload
566+
- def open(self):
567+
- ...
568+
+ def open(self): ...
569+
570+
571+
def fakehttp():
572+
```
573+
574+
552575

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
11
---
22
source: crates/ruff_python_formatter/tests/fixtures.rs
3-
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/assign_breaking.py
3+
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/preview.py
44
---
55
## Input
66
```py
7-
# Below is black stable style
8-
# In preview style, black always breaks the right side first
7+
"""
8+
Black's `Preview.module_docstring_newlines`
9+
"""
10+
first_stmt_after_module_level_docstring = 1
911
10-
if True:
12+
13+
class CachedRepository:
14+
# Black's `Preview.dummy_implementations`
15+
def get_release_info(self): ...
16+
17+
18+
def raw_docstring():
19+
20+
r"""Black's `Preview.accept_raw_docstrings`
21+
a
22+
b
23+
"""
24+
pass
25+
26+
27+
def reference_docstring_newlines():
28+
29+
"""A regular docstring for comparison
30+
a
31+
b
32+
"""
33+
pass
34+
35+
36+
class RemoveNewlineBeforeClassDocstring:
37+
38+
"""Black's `Preview.no_blank_line_before_class_docstring`"""
39+
40+
41+
def f():
42+
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
1143
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
1244
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
1345
] = cccccccc.ccccccccccccc.cccccccc
@@ -52,10 +84,41 @@ preview = Disabled
5284
```
5385

5486
```py
55-
# Below is black stable style
56-
# In preview style, black always breaks the right side first
87+
"""
88+
Black's `Preview.module_docstring_newlines`
89+
"""
90+
first_stmt_after_module_level_docstring = 1
91+
92+
93+
class CachedRepository:
94+
# Black's `Preview.dummy_implementations`
95+
def get_release_info(self):
96+
...
97+
98+
99+
def raw_docstring():
100+
r"""Black's `Preview.accept_raw_docstrings`
101+
a
102+
b
103+
"""
104+
pass
57105
58-
if True:
106+
107+
def reference_docstring_newlines():
108+
"""A regular docstring for comparison
109+
a
110+
b
111+
"""
112+
pass
113+
114+
115+
class RemoveNewlineBeforeClassDocstring:
116+
117+
"""Black's `Preview.no_blank_line_before_class_docstring`"""
118+
119+
120+
def f():
121+
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
59122
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
60123
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
61124
] = cccccccc.ccccccccccccc.cccccccc
@@ -100,10 +163,40 @@ preview = Enabled
100163
```
101164

102165
```py
103-
# Below is black stable style
104-
# In preview style, black always breaks the right side first
166+
"""
167+
Black's `Preview.module_docstring_newlines`
168+
"""
169+
first_stmt_after_module_level_docstring = 1
170+
171+
172+
class CachedRepository:
173+
# Black's `Preview.dummy_implementations`
174+
def get_release_info(self): ...
175+
176+
177+
def raw_docstring():
178+
r"""Black's `Preview.accept_raw_docstrings`
179+
a
180+
b
181+
"""
182+
pass
183+
184+
185+
def reference_docstring_newlines():
186+
"""A regular docstring for comparison
187+
a
188+
b
189+
"""
190+
pass
191+
192+
193+
class RemoveNewlineBeforeClassDocstring:
194+
195+
"""Black's `Preview.no_blank_line_before_class_docstring`"""
196+
105197
106-
if True:
198+
def f():
199+
"""Black's `Preview.prefer_splitting_right_hand_side_of_assignments`"""
107200
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa[
108201
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
109202
] = cccccccc.ccccccccccccc.cccccccc

0 commit comments

Comments
 (0)